diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 2cad5fca..4a38216e 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1.3 -ARG UV_VERSION=0.5.10 +ARG UV_VERSION=latest ARG CORE_IMAGE=ubuntu:noble FROM ghcr.io/astral-sh/uv:${UV_VERSION} AS astral-uv-source @@ -88,7 +88,11 @@ RUN apt update && apt install -y \ && rm -rf /tmp/* /var/tmp/* \ && rm -rf /var/lib/apt/lists/* - +# Node.js & npm for frontend build +RUN apt-get update && \ + apt-get install -y curl && \ + curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && \ + apt-get install -y nodejs WORKDIR /tmp diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2ead9957..d5eeaafa 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,11 +4,14 @@ "name": "aymurai", // "initializeCommand": "make core-build", "dockerComposeFile": "docker-compose.yml", - "service": "aymurai-devcontainer-gpu", + "service": "aymurai-devcontainer", "runServices": [ - "aymurai-devcontainer-gpu" + "aymurai-devcontainer" ], "workspaceFolder": "/workspace", + "mounts": [ + "source=codex-data,target=/home/ubuntu/.codex,type=volume" + ], "customizations": { "vscode": { "settings": { @@ -47,9 +50,11 @@ "cweijan.vscode-database-client2", "christian-kohler.path-intellisense", "github.vscode-github-actions", - "seatonjiang.gitmoji-vscode" + "seatonjiang.gitmoji-vscode", + "openai.chatgpt" ] } }, - "postCreateCommand": "bash /home/ubuntu/entrypoint.sh" + "postCreateCommand": "bash /home/ubuntu/entrypoint.sh", + "postStartCommand": "sudo chown -R ubuntu:ubuntu /home/ubuntu/.codex && sudo chmod -R u+rwX /home/ubuntu/.codex" } \ No newline at end of file diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 5e12b20d..c434079c 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -5,7 +5,7 @@ x-template: &template - ..:/workspace:cached - ../notebooks/:/notebooks - ../resources/:/resources - - ../test/:/test + - ../tests/:/tests - $HOME/.ssh/:/home/ubuntu/.ssh - /var/run/docker.sock:/var/run/docker.sock env_file: @@ -27,7 +27,9 @@ services: resources: reservations: devices: - - capabilities: [ gpu ] + - driver: nvidia + count: all + capabilities: [ gpu ] aymurai-devcontainer: <<: *template diff --git a/.devcontainer/entrypoint.sh b/.devcontainer/entrypoint.sh index 000c3360..5eddef91 100644 --- a/.devcontainer/entrypoint.sh +++ b/.devcontainer/entrypoint.sh @@ -1,7 +1,7 @@ #!/bin/sh # install dependencies -uv sync --frozen --all-extras +uv sync --frozen --all-extras --all-groups # configure precommit uv run pre-commit install diff --git a/.dockerignore b/.dockerignore index 9b91c7d3..f5fe77dd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,7 +2,10 @@ resources/ !resources/api !resources/pipelines !resources/cache/aymurai -!resources/cache/tfhub_modules notebooks/ -*.egg-info/ \ No newline at end of file +*.egg-info/ + +frontend/node_modules/ +frontend/out/ +frontend-dist/ diff --git a/.env.common b/.env.common index 5b5d5640..493770c9 100644 --- a/.env.common +++ b/.env.common @@ -1,31 +1,8 @@ -DATASETS_BASEPATH=/resources/datasets -MODELS_BASEPATH=/resources/models - +CACHE_PATH=/resources/cache AYMURAI_CACHE_BASEPATH=/resources/cache/aymurai -CACHE_PATH=/resources/cache -HF_DATASETS_CACHE=/resources/cache/huggingface/cache -TRANSFORMERS_CACHE=/resources/cache/huggingface/transformers +HF_HOME=/resources/cache/huggingface TOKENIZERS_PARALLELISM=1 -TESSDATA_PREFIX=/usr/local/share/tessdata - AYMURAI_RESTRICTED_DOCUMENT_PDFS_PATH="/resources/data/restricted/ar-juz-pcyf-10/RESOLUCIONES DEL JUZGADO-pdf" AYMURAI_RESTRICTED_DOCUMENT_DOCS_PATH="/resources/data/restricted/ar-juz-pcyf-10/RESOLUCIONES DEL JUZGADO" - -TF_CPP_MIN_LOG_LEVEL=3 -TFHUB_CACHE_DIR=/resources/cache/tfhub_modules - -TORCH_VERSION=2.0.1 -CUDA_VERSION=cu118 - -CORE_IMAGE_CUDA=registry.gitlab.com/collective.ai/datagenero-public/aymurai-core -CORE_IMAGE_CPU=registry.gitlab.com/collective.ai/datagenero-public/aymurai-core-cpu -API_IMAGE=registry.gitlab.com/collective.ai/datagenero-public/aymurai-api - -API_HOST=0.0.0.0 -API_PORT=8899 - -SRC_VOLUME_MOUNT=src:/src -RESOURCES_VOLUME_MOUNT=resources:/resources -NOTEBOOKS_VOLUME_MOUNT=notebooks:/notebooks diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 00000000..a180c827 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,87 @@ +name: pytest + +on: + pull_request: + types: + - opened + - synchronize + - reopened + - ready_for_review + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pr-tests-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + pytest: + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} + name: pytest (${{ matrix.os }}, py${{ matrix.python-version }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - windows-latest + python-version: + - "3.10" + # - "3.11" + # - "3.12" + # - "3.13" + # - "3.14" + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup uv + uses: astral-sh/setup-uv@v5 + with: + version: latest + enable-cache: true + cache-dependency-glob: uv.lock + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Configure es_AR locale (Ubuntu) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install --yes locales + sudo locale-gen es_AR.UTF-8 + sudo update-locale LANG=es_AR.UTF-8 LC_ALL=es_AR.UTF-8 + echo "LANG=es_AR.UTF-8" >> "$GITHUB_ENV" + echo "LC_ALL=es_AR.UTF-8" >> "$GITHUB_ENV" + locale -a + + - name: Install dependencies + run: | + uv sync --frozen --python python --no-dev --no-python-downloads --group tests + + - name: Run api tests + env: + DISKCACHE_ROOT: /tmp + SQLALCHEMY_DATABASE_URI: sqlite:////tmp/aymurai-test.db + run: uv run --no-sync pytest -q --tb=short --disable-warnings --color=yes --maxfail=5 tests/api + + - name: Download pipelines data + env: + DISKCACHE_ROOT: /tmp + RESOURCES_BASEPATH: resources + AYMURAI_CACHE_BASEPATH: resources/cache/aymurai + run: uv run --no-sync pipeline-download + + - name: Run pipeline tests + env: + DISKCACHE_ROOT: /tmp + RESOURCES_BASEPATH: resources + AYMURAI_CACHE_BASEPATH: resources/cache/aymurai + run: uv run --no-sync pytest -q --tb=short --disable-warnings --color=yes --maxfail=5 tests/integration/pipelines diff --git a/.gitignore b/.gitignore index 5f22f5f3..feb0089a 100755 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,16 @@ resources .venv aymurai/version.py + +.agents +.opencode.json +.sisyphus +notebooks/** +!notebooks/**/ +!notebooks/**/*.ipynb + +# Frontend build output +frontend-dist/ + +# pnpm local package store +.pnpm-store/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2aeb0810..8f7ef35f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +1,11 @@ repos: - - repo: https://github.com/ambv/black - rev: 22.6.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.7.2 hooks: - - id: black - language_version: python3.10 + - id: ruff + - id: ruff-format - repo: https://github.com/kynan/nbstripout rev: 0.6.0 hooks: - - id: nbstripout - - # - repo: https://github.com/pre-commit/pre-commit-hooks - # rev: v2.20.0 - # hooks: - # - id: flake8 \ No newline at end of file + - id: nbstripout \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..68007232 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,28 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "python-envs.defaultEnvManager": "ms-python.python:venv", + "python-envs.pythonProjects": [], + "files.associations": { + ".csv": "csv", + ".env*": "dotenv", + ".json": "json", + ".jsonc": "jsonc", + ".jsonl": "jsonl", + ".md": "markdown" + }, + "github.copilot.enable": { + "*": true, + "dotenv": false, + "csv": false, + "json": false, + "jsonc": false, + "jsonl": false, + "markdown": false, + "plaintext": false, + "scminput": false + } +} \ No newline at end of file diff --git a/Makefile b/Makefile index 64e27084..898fc3ae 100644 --- a/Makefile +++ b/Makefile @@ -3,20 +3,37 @@ export $(shell sed 's/=.*//' .env) include .env.common export $(shell sed 's/=.*//' .env.common) +# Select which API service to control (override with API_SERVICE=aymurai-api-gpu) +API_SERVICE ?= aymurai-api +# Select which full API service to control (override with API_FULL_SERVICE=aymurai-api-full-gpu) +API_FULL_SERVICE ?= aymurai-api-full + api-build: - docker compose build aymurai-api + docker compose build $(API_SERVICE) api-run: - docker compose run --service-ports aymurai-api + docker compose run --service-ports $(API_SERVICE) +api-up: + docker compose up -d $(API_SERVICE) +api-stop: + docker compose stop $(API_SERVICE) +api-logs: + docker compose logs -f $(API_SERVICE) api-pull: - docker compose pull aymurai-api + docker compose pull $(API_SERVICE) api-full-build: - docker compose build aymurai-api-full + docker compose build $(API_FULL_SERVICE) api-full-run: - docker compose run --service-ports aymurai-api-full + docker compose run --service-ports $(API_FULL_SERVICE) +api-full-up: + docker compose up -d $(API_FULL_SERVICE) +api-full-stop: + docker compose stop $(API_FULL_SERVICE) +api-full-logs: + docker compose logs -f $(API_FULL_SERVICE) api-full-pull: - docker compose pull aymurai-api-full + docker compose pull $(API_FULL_SERVICE) stress-test: locust -f locustfile.py --host http://localhost:8899 @@ -27,3 +44,19 @@ alembic-regenerate: cd aymurai && \ uv run alembic revision --autogenerate -m "Create database" && \ uv run alembic upgrade head + +# --- Frontend build --- +FRONTEND_DIR=frontend +FRONTEND_DIST_DIR=frontend-dist + +frontend-install: + cd $(FRONTEND_DIR) && pnpm install + +frontend-build: frontend-install + cd $(FRONTEND_DIR) && pnpm run build:web + # Copy build output to frontend-dist + rm -rf $(FRONTEND_DIST_DIR) + cp -r $(FRONTEND_DIR)/out/renderer $(FRONTEND_DIST_DIR) + +frontend-clean: + rm -rf $(FRONTEND_DIST_DIR) diff --git a/README.es.md b/README.es.md new file mode 100644 index 00000000..46d6072d --- /dev/null +++ b/README.es.md @@ -0,0 +1,173 @@ +# AymurAI +Idioma: [English](README.md) | **Español** + +AymurAI provee una aplicación integrada, una API y pipelines de ML para procesar resoluciones judiciales mediante dos flujos principales: + +- `anonymizer`: extracción de entidades y generación de documentos anonimizados. +- `data-public`: extracción de información estructurada para curación de dataset público. + +Este repositorio contiene el frontend React/Electron, el servicio FastAPI, las configuraciones de pipelines de producción y la persistencia en base de datos para ambos flujos. + +## Qué es AymurAI +AymurAI es un proyecto orientado a facilitar la generación de datos judiciales anonimizados y estructurados para casos de violencia de género (VG) en América Latina. La aplicación guía la ingesta de documentos, la inferencia de modelos, la revisión manual, la validación y la exportación de resultados para usos operativos y de investigación. + +El frontend puede ejecutarse como aplicación web servida por FastAPI o como aplicación de escritorio Electron. Permite cargar y previsualizar documentos, revisar anotaciones, configurar políticas de entidades, exportar documentos anonimizados y validar datasets estructurados. + +## Documentación +- Índice técnico: [docs/es/README.md](docs/es/README.md) +- Referencia de API: [docs/es/api/README.md](docs/es/api/README.md) +- Índice de pipelines: [docs/es/pipelines/README.md](docs/es/pipelines/README.md) +- Flujo anonymizer: [docs/es/pipelines/anonymizer/README.md](docs/es/pipelines/anonymizer/README.md) +- Flujo datapublic: [docs/es/pipelines/datapublic/README.md](docs/es/pipelines/datapublic/README.md) +- Esquema de base de datos interna: [docs/es/database/README.md](docs/es/database/README.md) +- Desarrollo y empaquetado del frontend: [frontend/README.md](frontend/README.md) + +## Inicio Rápido (imagen Docker) +Ejecutar la imagen full de la API (incluye recursos de producción): + +```bash +docker run -d --name aymurai-backend -p 8899:8899 ghcr.io/aymurai/api:full +``` + +Opcional: persistir DB/cache fuera del container (volumen host montado en `/resources/cache`): + +```bash +mkdir -p ./aymurai-cache + +docker run -d --name aymurai-backend -p 8899:8899 \ + -v "$(pwd)/aymurai-cache:/resources/cache" \ + ghcr.io/aymurai/api:full +``` + +Opcional: runtime con GPU (requiere NVIDIA Container Toolkit): + +```bash +docker run -d --name aymurai-backend-gpu --gpus all \ + -e TORCH_DEVICE=cuda \ + -p 8899:8899 \ + ghcr.io/aymurai/api:full +``` + +Abrir Swagger UI: + +```text +http://localhost:8899/api/docs +``` + +Abrir el frontend integrado: + +```text +http://localhost:8899/ +``` + +## Inicio Rápido (Docker Compose) +Usar los servicios definidos en `docker-compose.yml`: + +```bash +# CPU, perfil liviano +make api-up + +# CPU, perfil full +make api-full-up + +# GPU, perfil liviano +API_SERVICE=aymurai-api-gpu make api-up + +# GPU, perfil full +API_FULL_SERVICE=aymurai-api-full-gpu make api-full-up +``` + +Ver logs: + +```bash +make api-logs +# o make api-full-logs +``` + +## Resumen de runtime +- Framework: `FastAPI` +- Puerto por defecto: `8899` +- Ruta del frontend integrado: `GET /` +- Implementaciones del frontend: navegador y Electron +- Motor de DB: `SQLModel` + migraciones Alembic al iniciar +- URI de DB por defecto: `sqlite:////resources/cache/sqlite/database.db` +- Configs de pipeline de producción: + - `resources/pipelines/production/flair-anonymizer/pipeline.json` + - `resources/pipelines/production/datapublic/pipeline.json` + +## Endpoints públicos principales +- `GET /api/server/healthcheck` +- `GET /api/server/stats/summary` +- `POST /api/misc/document-extract` (y alias deprecado `POST /api/document-extract`) +- `POST /api/anonymizer/predict` +- `POST /api/anonymizer/disambiguate` +- `POST /api/anonymizer/validation` +- `POST /api/anonymizer/anonymize-document` +- `POST /api/datapublic/predict/{document_id}` +- `GET /api/datapublic/validation/document/{document_id}` +- `POST /api/datapublic/validation/document/{document_id}` + +Para contratos request/response y ejemplos completos, ver [docs/es/api/README.md](docs/es/api/README.md). + +## Despliegue en red cerrada +Para mover una imagen a un entorno sin internet: + +```bash +docker image save ghcr.io/aymurai/api:full -o aymurai-api-full.tar +docker load -i aymurai-api-full.tar +``` + +## Contribución +Las contribuciones son bienvenidas en documentación, API y mejoras de pipelines. + +- Guía de contribución: [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) +- Seguridad y ética: [docs/SECURITY.md](docs/SECURITY.md) +- Código de conducta: [docs/CODE_OF_CONDUCT.md](docs/CODE_OF_CONDUCT.md) + +## Contribuidores +- **Julián Ansaldo** - [@jansaldo](https://github.com/jansaldo) at [collective.ai](https://collectiveai.io) ([email](mailto:juli@collectiveai.io)) +- **Raúl Barriga** - [@jedzill4](https://github.com/jedzill4) at [collective.ai](https://collectiveai.io) ([email](mailto:r@collectiveai.io)) +- **Sofía del Pozo** - [@sofiadelpozo](https://github.com/sofiadelpozo) at [collective.ai](https://collectiveai.io) ([email](mailto:sofia.delpozo@collectiveai.io)) +- **Paolo Donizetti** - [@padonizetti](https://github.com/padonizetti) at [collective.ai](https://collectiveai.io) ([email](mailto:paolo@collectiveai.io)) +- **Conrado Beatriz** - [@conrabeatriz](https://github.com/conrabeatriz) at [collective.ai](https://collectiveai.io) ([email](mailto:conrado@collectiveai.io)) +- **Lionel Chamorro** - [@lionelchamorro](https://github.com/lionelchamorro) at [collective.ai](https://collectiveai.io) ([email](mailto:lio@collectiveai.io)) +- **Damián Mazzini** - [@ +dmazzini](https://github.com/dmazzini) at [collective.ai](https://collectiveai.io) ([email](mailto:dami@collectiveai.io)) + +## Citar AymurAI +Si usás AymurAI en investigación o publicaciones, por favor citá: + +```bibtex +@techreport{feldfeber2022, + author = {Feldfeber, Ivana and Quiroga, Yasm\'{\i}n Bel\'{e}n and Guevara, Clarissa and Ciolfi Felice, Marianela}, + title = {Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico}, + institution = {DataGenero}, + year = {2022}, + url = {https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view} +} +``` + +``` +@inproceedings{10.1145/3706598.3713681, + author = {Ciolfi Felice, Marianela and Feldfeber, Ivana and Glasserman Apicella, Carolina and Quiroga, Yasm\'{\i}n Bel\'{e}n and Ansaldo, Juli\'{a}n and Lapenna, Luciano and Bezchinsky, Santiago and Barriga Rubio, Ra\'{u}l and Garc\'{\i}a, Mail\'{e}n}, + title = {Doing the Feminist Work in AI: Reflections from an AI Project in Latin America}, + booktitle = {Proceedings of the 2025 CHI Conference on Human Factors in Computing Systems}, + series = {CHI '25}, + year = {2025}, + isbn = {9798400713941}, + publisher = {Association for Computing Machinery}, + address = {New York, NY, USA}, + doi = {10.1145/3706598.3713681}, + url = {https://doi.org/10.1145/3706598.3713681}, + abstract = {The contemporary AI development landscape is dominated by big corporations, lacks diversity, and mostly centres the Global North, or applies extractivist logics in the South. This paper showcases a feminist process of AI development from Latin America, where we created an interactive, AI-powered tool that helps criminal court officers open justice data, addressing a data gap on gender-based violence. Through a collaborative autoethnography, drawing from Latin American feminisms, we unpack and visibilize the feminist work that was required, as a crucial step to counter hegemonic narratives. Foregrounding the subjugated knowledges of our experiences, we offer a concrete example of a feminist approach to AI development grounded in practice. With this, we aim to critically inspire those who consider building technology in service of social justice causes, or who choose to build AI systems otherwise.}, + articleno = {998}, + numpages = {18}, + keywords = {Global South, NGO, activism, critical HCI, critical computing, duoethnography, feminist AI, feminist research}, + location = {}, +} +``` + +Para usar la cita más actualizada, consulta la referencia del proyecto en el repositorio de la organización: [github.com/aymurai](https://github.com/aymurai). + +## Licencia +AymurAI es software de código abierto bajo licencia [MIT](LICENSE.md). Esta licencia permite modificar, distribuir y usar de forma privada el software, siempre que se mantenga el crédito correspondiente a la autoría original. diff --git a/README.md b/README.md index d140e5ad..a8c335f1 100644 --- a/README.md +++ b/README.md @@ -1,97 +1,173 @@ -# AymurAI Backend -This repository contains the backend API and machine learning models for [AymurAI](https://www.aymurai.info), a tool designed to generate anonymized datasets from judicial rulings related to gender-based violence (GBV). +# AymurAI +Language: **English** | [Español](README.es.md) -AymurAI's backend is responsible for managing the interaction between the frontend and the machine learning models. It provides an API that handles data input, automates the extraction of information from court rulings, and document edition for anonymization purposes. +AymurAI provides an integrated application, API, and ML pipelines for processing judicial rulings through two main workflows: +- `anonymizer`: extract named entities and produce anonymized documents. +- `data-public`: extract structured information for public dataset curation. -## Table of Contents -* [About AymurAI, its Uses and Limitations](#about-aymurai-its-uses-and-limitations) -* [Deployment](#deployment) -* [Pipeline](#pipeline) -* [Tutorials](#tutorials) -* [Contributing](#contributing) -* [Contributors](#contributors) -* [Citing AymurAI](#citing-aymurai) -* [License](#license) +This repository contains the React/Electron frontend, FastAPI service, production pipeline configurations, and database persistence used by both workflows. +## About AymurAI +AymurAI is a project focused on supporting the generation of anonymized and structured judicial data for gender-based violence (GBV) cases in Latin America. The application guides users through document ingestion, ML inference, manual review, validation, and export for operational and research uses. -## About AymurAI, its Uses and Limitations -AymurAI is a tool designed to address the lack of available data in the judicial system regarding gender-based violence (GBV) rulings in Latin America. Its goal is to increase report levels, build trust in the justice system, and improve access to justice for women and LGBTIQ+ people. AymurAI generates and maintains anonymized datasets from legal rulings to better understand GBV and support policy-making, while also contributing to campaigns run by feminist collectives. +The frontend can run as a web application served by FastAPI or as an Electron desktop application. It supports document upload and preview, interactive annotation review, entity policy configuration, anonymized document export, and structured dataset validation. -AymurAI is still a prototype and is currently only implemented in Criminal Court N°10 in the City of Buenos Aires, Argentina. Its capabilities are limited to semi-automated data collection and analysis. The quality, consistency, and availability of the data, as well as cooperation from court officials and the broader cultural and political context, may affect its results. +## Documentation +- Technical docs index: [docs/README.md](docs/README.md) +- API reference: [docs/api/README.md](docs/api/README.md) +- Pipelines index: [docs/pipelines/README.md](docs/pipelines/README.md) +- Anonymizer flow: [docs/pipelines/anonymizer/README.md](docs/pipelines/anonymizer/README.md) +- Datapublic flow: [docs/pipelines/datapublic/README.md](docs/pipelines/datapublic/README.md) +- Internal database schema: [docs/database/README.md](docs/database/README.md) +- Frontend development and packaging: [frontend/README.md](frontend/README.md) -The models were trained using closed datasets from an Argentine criminal court and are specifically tailored to extract relevant information from GBV-related rulings. The domain-specific training ensures the models' accuracy within this legal and cultural context, though they may not be applicable to other regions with different legal systems or cultural norms. +## Quick Start (Docker Image) +Run the full API image (includes production resources): +```bash +docker run -d --name aymurai-backend -p 8899:8899 ghcr.io/aymurai/api:full +``` -## Deployment -AymurAI's backend is deployed using [Docker](https://www.docker.com/). The Docker images are available at the following registry: +Optional: persist DB/cache outside the container (host volume mounted at `/resources/cache`): ```bash -ghcr.io/aymurai/api:full +mkdir -p ./aymurai-cache + +docker run -d --name aymurai-backend -p 8899:8899 \ + -v "$(pwd)/aymurai-cache:/resources/cache" \ + ghcr.io/aymurai/api:full ``` -### Quick Start -To deploy a production-ready instance of the API, run: +Optional: GPU runtime (requires NVIDIA Container Toolkit): ```bash -docker run -d -p 8899:8899 ghcr.io/aymurai/api:full +docker run -d --name aymurai-backend-gpu --gpus all \ + -e TORCH_DEVICE=cuda \ + -p 8899:8899 \ + ghcr.io/aymurai/api:full ``` -This command will start the API on port `8899` on your local machine. You can access the API documentation through OpenAPI at: +Open Swagger UI: +```text +http://localhost:8899/api/docs ``` -http://localhost:8899/docs -``` - -Once it is deployed, it doesn't require an internet connection to work. -### Running on a Closed Network -If you need to deploy in an environment without internet access, export the Docker image by running: +Open the bundled frontend: -```bash -docker image save ghcr.io/aymurai/api:full -o aymurai-api.tar +```text +http://localhost:8899/ ``` -Transfer the image to the target machine and load it: +## Quick Start (Docker Compose) +Use the services defined in `docker-compose.yml`: ```bash -docker load -i aymurai-api.tar -``` +# CPU, lightweight API profile +make api-up -For more information on Docker deployment, refer to the [Docker documentation](https://docs.docker.com/). If you need further assistance, feel free to contact us at [aymurai@datagenero.org](mailto:aymurai@datagenero.org). +# CPU, full API profile +make api-full-up +# GPU, lightweight API profile +API_SERVICE=aymurai-api-gpu make api-up + +# GPU, full API profile +API_FULL_SERVICE=aymurai-api-full-gpu make api-full-up +``` -## Pipeline -AymurAI’s backend utilizes a structured data processing pipeline to handle anonymized legal rulings and extract relevant information. This data is processed and made accessible via the API. For more details, please refer to the [pipeline documentation](docs/pipeline/README.md). +Check logs: +```bash +make api-logs +# or make api-full-logs +``` -## Tutorials -To get started with AymurAI, refer to our [tutorials](tutorials/GET_STARTED.md). These guides provide step-by-step instructions on setting up and using the AymurAI backend, including configuration, example queries, and more. +## Runtime Overview +- Framework: `FastAPI` +- Default API port: `8899` +- Bundled frontend path: `GET /` +- Frontend implementations: browser and Electron +- DB engine: `SQLModel` + Alembic migrations on startup +- Default DB URI: `sqlite:////resources/cache/sqlite/database.db` +- Production pipeline configs: + - `resources/pipelines/production/flair-anonymizer/pipeline.json` + - `resources/pipelines/production/datapublic/pipeline.json` + +## Main Public Endpoints +- `GET /api/server/healthcheck` +- `GET /api/server/stats/summary` +- `POST /api/misc/document-extract` (and deprecated alias `POST /api/document-extract`) +- `POST /api/anonymizer/predict` +- `POST /api/anonymizer/disambiguate` +- `POST /api/anonymizer/validation` +- `POST /api/anonymizer/anonymize-document` +- `POST /api/datapublic/predict/{document_id}` +- `GET /api/datapublic/validation/document/{document_id}` +- `POST /api/datapublic/validation/document/{document_id}` + +For full request/response contracts and examples, see [docs/api/README.md](docs/api/README.md). + +## Closed-Network Deployment +To move an image into a closed environment: +```bash +docker image save ghcr.io/aymurai/api:full -o aymurai-api-full.tar +docker load -i aymurai-api-full.tar +``` ## Contributing -Thank you for your interest in contributing to AymurAI! There are many ways to get involved, from improving documentation to enhancing the codebase. To get started, please review our [contributor guidelines](docs/CONTRIBUTING.md) and our [code of conduct](docs/CODE_OF_CONDUCT.md). We welcome contributions in areas such as expanding the API and improving the data processing pipeline. +Contributions are welcome across documentation, API, and pipeline improvements. +- Contributing guide: [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) +- Security and ethics: [docs/SECURITY.md](docs/SECURITY.md) +- Code of conduct: [docs/CODE_OF_CONDUCT.md](docs/CODE_OF_CONDUCT.md) ## Contributors -* **Julián Ansaldo** - [@jansaldo](https://github.com/jansaldo) at [collective.ai](https://collectiveai.io) ([email](mailto:juli@collectiveai.io)) -* **Raúl Barriga** - [@jedzill4](https://github.com/jedzill4) at [collective.ai](https://collectiveai.io) ([email](mailto:r@collectiveai.io)) - +- **Julián Ansaldo** - [@jansaldo](https://github.com/jansaldo) at [collective.ai](https://collectiveai.io) ([email](mailto:juli@collectiveai.io)) +- **Raúl Barriga** - [@jedzill4](https://github.com/jedzill4) at [collective.ai](https://collectiveai.io) ([email](mailto:r@collectiveai.io)) +- **Sofía del Pozo** - [@sofiadelpozo](https://github.com/sofiadelpozo) at [collective.ai](https://collectiveai.io) ([email](mailto:sofia.delpozo@collectiveai.io)) +- **Paolo Donizetti** - [@padonizetti](https://github.com/padonizetti) at [collective.ai](https://collectiveai.io) ([email](mailto:paolo@collectiveai.io)) +- **Conrado Beatriz** - [@conrabeatriz](https://github.com/conrabeatriz) at [collective.ai](https://collectiveai.io) ([email](mailto:conrado@collectiveai.io)) +- **Lionel Chamorro** - [@lionelchamorro](https://github.com/lionelchamorro) at [collective.ai](https://collectiveai.io) ([email](mailto:lio@collectiveai.io)) +- **Damián Mazzini** - [@ +dmazzini](https://github.com/dmazzini) at [collective.ai](https://collectiveai.io) ([email](mailto:dami@collectiveai.io)) ## Citing AymurAI -If you use AymurAI in your research or any publication, please cite the following paper to acknowledge our work: +If you use AymurAI in research or publications, please cite: ```bibtex @techreport{feldfeber2022, - author = "Feldfeber, Ivana and Quiroga, Yasmín Belén and Guevara, Clarissa and Ciolfi Felice, Marianela", - title = "Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico", - institution = "DataGenero", - year = "2022", - url = "https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view" + author = {Feldfeber, Ivana and Quiroga, Yasm\'{\i}n Bel\'{e}n and Guevara, Clarissa and Ciolfi Felice, Marianela}, + title = {Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico}, + institution = {DataGenero}, + year = {2022}, + url = {https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view} +} +``` + +``` +@inproceedings{10.1145/3706598.3713681, + author = {Ciolfi Felice, Marianela and Feldfeber, Ivana and Glasserman Apicella, Carolina and Quiroga, Yasm\'{\i}n Bel\'{e}n and Ansaldo, Juli\'{a}n and Lapenna, Luciano and Bezchinsky, Santiago and Barriga Rubio, Ra\'{u}l and Garc\'{\i}a, Mail\'{e}n}, + title = {Doing the Feminist Work in AI: Reflections from an AI Project in Latin America}, + booktitle = {Proceedings of the 2025 CHI Conference on Human Factors in Computing Systems}, + series = {CHI '25}, + year = {2025}, + isbn = {9798400713941}, + publisher = {Association for Computing Machinery}, + address = {New York, NY, USA}, + doi = {10.1145/3706598.3713681}, + url = {https://doi.org/10.1145/3706598.3713681}, + abstract = {The contemporary AI development landscape is dominated by big corporations, lacks diversity, and mostly centres the Global North, or applies extractivist logics in the South. This paper showcases a feminist process of AI development from Latin America, where we created an interactive, AI-powered tool that helps criminal court officers open justice data, addressing a data gap on gender-based violence. Through a collaborative autoethnography, drawing from Latin American feminisms, we unpack and visibilize the feminist work that was required, as a crucial step to counter hegemonic narratives. Foregrounding the subjugated knowledges of our experiences, we offer a concrete example of a feminist approach to AI development grounded in practice. With this, we aim to critically inspire those who consider building technology in service of social justice causes, or who choose to build AI systems otherwise.}, + articleno = {998}, + numpages = {18}, + keywords = {Global South, NGO, activism, critical HCI, critical computing, duoethnography, feminist AI, feminist research}, + location = {}, } ``` -Proper citation helps us continue developing AymurAI and supporting the community. +For the most up-to-date citation, please use the project-level reference in the organization repository: [github.com/aymurai](https://github.com/aymurai). ## License -AymurAI is open-source software licensed under the [MIT License](LICENSE.md). This license allows for modification, distribution, and private use, provided that appropriate credit is given to the original authors. +AymurAI is open-source software licensed under the [MIT License](LICENSE.md). This license allows modification, distribution, and private use, provided that appropriate credit is given to the original authors. diff --git a/aymurai/alembic.ini b/aymurai/alembic.ini index b9a6bd2c..87f2df84 100644 --- a/aymurai/alembic.ini +++ b/aymurai/alembic.ini @@ -68,11 +68,11 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne # on newly generated revision scripts. See the documentation for further # detail and examples -# format using "black" - use the console_scripts runner, against the "black" entrypoint -# hooks = black -# black.type = console_scripts -# black.entrypoint = black -# black.options = -l 79 REVISION_SCRIPT_FILENAME +# format using "ruff format" - use the exec runner, execute a binary +# hooks = ruff_format +# ruff_format.type = exec +# ruff_format.executable = ruff +# ruff_format.options = format REVISION_SCRIPT_FILENAME # lint with attempts to fix using "ruff" - use the exec runner, execute a binary # hooks = ruff diff --git a/aymurai/api/core.py b/aymurai/api/core.py index c9223455..185dba4f 100644 --- a/aymurai/api/core.py +++ b/aymurai/api/core.py @@ -1,9 +1,7 @@ from fastapi.routing import APIRouter from .endpoints.routers.anonymizer import anonymizer -from .endpoints.routers.anonymizer import database as anonymizer_database from .endpoints.routers.datapublic import datapublic - from .endpoints.routers.misc import convert, document_extract from .endpoints.routers.server import stats @@ -23,11 +21,6 @@ prefix="/anonymizer", tags=["anonymization/model"], ) -# router.include_router( -# anonymizer_database.router, -# prefix="/anonymizer/database", -# tags=["anonymization/database"], -# ) # Datapublic router.include_router( diff --git a/aymurai/api/endpoints/routers/anonymizer/anonymizer.py b/aymurai/api/endpoints/routers/anonymizer/anonymizer.py index 00a562dd..1be1a121 100644 --- a/aymurai/api/endpoints/routers/anonymizer/anonymizer.py +++ b/aymurai/api/endpoints/routers/anonymizer/anonymizer.py @@ -2,10 +2,10 @@ import os import subprocess import tempfile +from collections.abc import Iterable from threading import Lock -import torch -from fastapi import Body, Depends, Form, Query, UploadFile +from fastapi import Body, Depends, Form, HTTPException, Query, UploadFile from fastapi.responses import FileResponse from fastapi.routing import APIRouter from sqlmodel import Session @@ -18,7 +18,7 @@ anonymization_paragraph_create, anonymization_paragraph_read, ) -from aymurai.database.schema import AnonymizationParagraph +from aymurai.database.schema import AnonymizationParagraph, AnonymizationParagraphCreate from aymurai.database.session import get_session from aymurai.database.utils import data_to_uuid, text_to_uuid from aymurai.logger import get_logger @@ -26,24 +26,308 @@ DocLabel, DocumentAnnotations, DocumentInformation, + LabelPolicy, + RenderPolicy, TextRequest, ) -from aymurai.settings import settings -from aymurai.text.anonymization import DocAnonymizer +from aymurai.settings import DEFAULT_RENDER_POLICY, settings +from aymurai.text.anonymization import ( + InvalidDocumentAnonymizer, + get_anonymizer, +) from aymurai.text.extraction import MIMETYPE_EXTENSION_MAPPER +from aymurai.utils.entity_disambiguation import ( + build_canonical_entities, + get_canonical_dates, + map_canonical_entities_ner_preds, +) from aymurai.utils.misc import get_element logger = get_logger(__name__) RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH -torch.set_num_threads = 100 # FIXME: polemic ? pipeline_lock = Lock() router = APIRouter() +def _entities_to_doclabels(entities: list[dict]) -> list[DocLabel]: + """ + Convert raw entities to DocLabel objects. + + Args: + entities (list[dict]): List of entity dictionaries. + + Returns: + list[DocLabel]: List of DocLabel objects. + """ + doclabels: list[DocLabel] = [] + + for ent in entities: + try: + if isinstance(ent, DocLabel): + doclabel = ent + else: + doclabel = DocLabel.model_validate( + { + "text": ent.get("text", ""), + "start_char": ent.get("start_char"), + "end_char": ent.get("end_char"), + "attrs": ent.get("attrs", {}), + } + ) + doclabels.append(doclabel) + except Exception as exc: # keep going if a single entity is malformed + logger.warning(f"Skipping invalid entity for DocLabel: {exc}") + + return _dedupe_doclabels(doclabels) + + +def _dedupe_doclabels(labels: Iterable[DocLabel]) -> list[DocLabel]: + """ + Merge duplicate labels for the same span and AymurAI label. + + Args: + labels (Iterable[DocLabel]): An iterable of DocLabel objects, + potentially containing duplicates. + + Returns: + list[DocLabel]: A list of DocLabel objects with duplicates merged, + preserving the order of first occurrence. + """ + deduped: list[DocLabel] = [] + index_by_key: dict[tuple[int, int, str], int] = {} + + for label in labels: + key = ( + label.start_char, + label.end_char, + label.attrs.aymurai_label if label.attrs else "", + ) + existing_index = index_by_key.get(key) + if existing_index is None: + index_by_key[key] = len(deduped) + deduped.append(label) + continue + + existing = deduped[existing_index] + existing_data = existing.model_dump(mode="json") + incoming_data = label.model_dump(mode="json") + existing_attrs = existing_data.get("attrs") or {} + incoming_attrs = incoming_data.get("attrs") or {} + + for attr_key, incoming_value in incoming_attrs.items(): + existing_value = existing_attrs.get(attr_key) + if attr_key == "aymurai_label_subclass": + merged = list(existing_value or []) + for subclass in incoming_value or []: + if subclass not in merged: + merged.append(subclass) + existing_attrs[attr_key] = merged + elif existing_value in (None, [], "") and incoming_value not in ( + None, + [], + "", + ): + existing_attrs[attr_key] = incoming_value + + existing_data["attrs"] = existing_attrs + deduped[existing_index] = DocLabel.model_validate(existing_data) + + return deduped + + +def _merge_label_policies( + request_policies: dict[str, LabelPolicy] | None, +) -> dict[str, LabelPolicy]: + """ + Merges label policies from settings and request, with request policies taking precedence. + + Args: + request_policies (dict[str, LabelPolicy] | None): Per-label policies provided in the request body. + + Returns: + dict[str, LabelPolicy]: Effective per-label policies after merging settings and request policies. + """ + policies: dict[str, LabelPolicy] = {} + + if settings.DISAMBIGUATION_LABEL_POLICIES: + for label, policy in settings.DISAMBIGUATION_LABEL_POLICIES.items(): + incoming = LabelPolicy.model_validate(policy) + current = policies.get(label, LabelPolicy()) + if incoming.disambiguation is not None: + current.disambiguation = incoming.disambiguation + if incoming.anonymize is not None: + current.anonymize = incoming.anonymize + if incoming.use_subclass_when_available is not None: + current.use_subclass_when_available = ( + incoming.use_subclass_when_available + ) + policies[label] = current + + if request_policies: + for label, policy in request_policies.items(): + incoming = LabelPolicy.model_validate(policy) + current = policies.get(label, LabelPolicy()) + if incoming.disambiguation is not None: + current.disambiguation = incoming.disambiguation + if incoming.anonymize is not None: + current.anonymize = incoming.anonymize + if incoming.use_subclass_when_available is not None: + current.use_subclass_when_available = ( + incoming.use_subclass_when_available + ) + policies[label] = current + + return policies + + +def _merge_render_policy( + request_policy: RenderPolicy | None, +) -> RenderPolicy: + """ + Merges render policies from settings and request, with request policy taking precedence. + + Args: + request_policy (RenderPolicy | None): Render policy from request. + + Returns: + RenderPolicy: Effective render policy. + """ + policy = RenderPolicy.model_validate(DEFAULT_RENDER_POLICY) + + def apply(incoming: RenderPolicy) -> None: + nonlocal policy + if incoming.suffix_mode is not None: + policy.suffix_mode = incoming.suffix_mode + if incoming.suffix_threshold is not None: + policy.suffix_threshold = incoming.suffix_threshold + + if settings.RENDER_POLICY: + apply(RenderPolicy.model_validate(settings.RENDER_POLICY)) + + if request_policy: + apply(RenderPolicy.model_validate(request_policy)) + + return policy + + +def _resolve_token_base( + label: DocLabel, + label_policy: LabelPolicy, +) -> str: + """ + Resolves the base token label for rendering. + + Args: + label (DocLabel): Label to render. + label_policy (LabelPolicy): Label policy rules. + + Returns: + str: Base token label (e.g., PER, DENUNCIANTE). + """ + attrs = label.attrs + if attrs is None: + return label.text + + subclass = None + if attrs.aymurai_label_subclass: + subclass = attrs.aymurai_label_subclass[0] + + if label_policy.use_subclass_when_available and subclass: + return subclass.upper() + + return attrs.aymurai_label + + +def _build_render_context( + annotations: list[DocumentInformation], + render_policy: RenderPolicy, + label_policies: dict[str, LabelPolicy], +) -> dict: + """ + Builds render context with per-entity indices and counts. + + Args: + annotations (list[DocumentInformation]): Document annotations. + render_policy (RenderPolicy): Render policy rules. + label_policies (dict[str, LabelPolicy]): Per-label policies. + + Returns: + dict: Render context with policy, indices, and counts. + """ + occurrences: list[tuple[int, int, str, str]] = [] + + for p_idx, paragraph in enumerate(annotations): + for label in paragraph.labels or []: + label_policy = label_policies.get( + label.attrs.aymurai_label + if label.attrs and label.attrs.aymurai_label + else None, + LabelPolicy(), + ) + base = _resolve_token_base(label, label_policy) + entity_id = ( + str(label.attrs.canonical_entity_id) + if label.attrs and label.attrs.canonical_entity_id + else label.text + ) + occurrences.append((p_idx, label.start_char, base, entity_id)) + + occurrences.sort(key=lambda item: (item[0], item[1])) + + index_by_entity: dict[tuple[str, str], int] = {} + next_index_by_base: dict[str, int] = {} + + for _, _, base, entity_id in occurrences: + key = (base, entity_id) + if key not in index_by_entity: + next_index_by_base[base] = next_index_by_base.get(base, 0) + 1 + index_by_entity[key] = next_index_by_base[base] + + count_by_base = {base: count for base, count in next_index_by_base.items()} + + return { + "render_policy": render_policy, + "label_policies": label_policies, + "index_by_entity": index_by_entity, + "count_by_base": count_by_base, + } + + +def _should_anonymize_label( + label: DocLabel, + label_policies: dict[str, LabelPolicy], +) -> bool: + """ + Determines whether a given label should be anonymized based on its attributes and the effective label policies. + + Args: + label (DocLabel): The document label to evaluate for anonymization. + label_policies (dict[str, LabelPolicy]): Effective per-label policies that may override default anonymization behavior. + + Returns: + bool: True if the label should be anonymized, False otherwise. + """ + if label.attrs and label.attrs.aymurai_anonymize is not None: + return bool(label.attrs.aymurai_anonymize) + + policy = ( + label_policies.get(str(label.attrs.aymurai_label).strip().upper()) + if label.attrs and label.attrs.aymurai_label + else None + ) + if policy is None: + return True + + if policy.anonymize is None: + return True + + return bool(policy.anonymize) + + # MARK: Predict @router.post("/predict", response_model=DocumentInformation) async def anonymizer_paragraph_predict( @@ -78,8 +362,8 @@ async def anonymizer_paragraph_predict( logger.info(f"cache loaded from key: {paragraph_id}") logger.debug(f"{cached_prediction}") - labels = cached_prediction.prediction - return DocumentInformation(document=cached_prediction.text, labels=labels or []) + labels = _entities_to_doclabels(cached_prediction.prediction or []) + return DocumentInformation(document=cached_prediction.text, labels=labels) logger.info("Running prediction") item = [{"path": "empty", "data": {"doc.text": text_request.text}}] @@ -93,18 +377,153 @@ async def anonymizer_paragraph_predict( processed = pipeline.postprocess([processed]) text = get_element(processed[0], ["data", "doc.text"]) or "" - labels = get_element(processed[0], ["predictions", "entities"]) or [] + raw_entities = get_element(processed[0], ["predictions", "entities"]) or [] + labels = _entities_to_doclabels(raw_entities) if use_cache: logger.info(f"saving in cache: {paragraph_id}") - paragraph = AnonymizationParagraph( - id=paragraph_id, + paragraph = AnonymizationParagraphCreate( text=text, prediction=labels, ) paragraph = anonymization_paragraph_create(paragraph, session=session) - return DocumentInformation(document=text, labels=paragraph.prediction) + return DocumentInformation(document=text, labels=labels) + + +# MARK: Disambiguate +@router.post("/disambiguate", response_model=DocumentAnnotations) +async def anonymizer_disambiguate( + paragraphs: list[DocumentInformation] = Body( + ..., + description=( + "List of per-paragraph predictions returned by /anonymizer/predict." + ), + ), + label_policies: dict[str, LabelPolicy] | None = Body( + None, + description=( + "Optional per-label policy overrides for disambiguation/anonymization." + ), + ), + session: Session = Depends(get_session), +) -> DocumentAnnotations: + """ + Performs canonical entity disambiguation using fuzzy matching. + + Args: + paragraphs: A list of DocumentInformation objects containing the NER + predictions per paragraph that need to be disambiguated. + label_policies: Optional per-label disambiguation/anonymization policies. + session: Database session dependency for caching results. + Returns: + DocumentAnnotations: The original annotations enriched with + 'canonical_entity_id' and 'role' fields for each resolved mention. + """ + logger.info( + "disambiguation start: paragraphs=%d", + len(paragraphs), + ) + + labels = [label for paragraph in paragraphs for label in (paragraph.labels or [])] + effective_label_policies = _merge_label_policies(label_policies) + logger.info("disambiguation labels: %d", len(labels)) + + all_detected_labels = { + label.attrs.aymurai_label + for label in labels + if label.attrs + and label.attrs.aymurai_label + and effective_label_policies.get(label.attrs.aymurai_label) + and effective_label_policies.get(label.attrs.aymurai_label).anonymize + } + + fuzzy_labels: set[str] = set() + + for label in all_detected_labels: + policy = effective_label_policies.get(label) + if policy and policy.disambiguation == "fuzzy": + fuzzy_labels.add(label) + continue + if policy and policy.disambiguation == "none": + continue + + fuzzy_labels.add(label) + + effective_disambiguation_by_label: dict[str, str] = {} + for label in all_detected_labels: + if label in fuzzy_labels: + effective_disambiguation_by_label[label] = "fuzzy" + else: + effective_disambiguation_by_label[label] = "none" + logger.info( + "disambiguation targets: detected=%s fuzzy=%s", + sorted(all_detected_labels), + sorted(fuzzy_labels), + ) + + canonical_entities = ( + build_canonical_entities( + labels, + target_labels=[label for label in fuzzy_labels if label != "FECHA"] + if fuzzy_labels + else None, + threshold=settings.THRESHOLD, + ) + if fuzzy_labels + else [] + ) + + logger.info("canonical entities detected were: %s", canonical_entities) + + if "FECHA" in fuzzy_labels: + canonical_entities += get_canonical_dates(labels) + + logger.info("canonical entities after adding dates: %s", canonical_entities) + + logger.info( + "fuzzy clustering produced %d canonical entities", len(canonical_entities) + ) + + logger.info( + "disambiguation merge: total=%d", + len(canonical_entities), + ) + + predictions = map_canonical_entities_ner_preds( + predictions=paragraphs, + canonical_entities=canonical_entities, + ) + + for document in predictions: + document.labels = _dedupe_doclabels(document.labels or []) + for label in document.labels or []: + label.attrs.aymurai_disambiguation = effective_disambiguation_by_label.get( + label.attrs.aymurai_label, "fuzzy" + ) + + policy = effective_label_policies.get(label.attrs.aymurai_label) + label.attrs.aymurai_anonymize = ( + policy.anonymize if policy and policy.anonymize is not None else True + ) + + paragraph_updates = [ + AnonymizationParagraphCreate( + text=paragraph.document, + prediction=paragraph.labels or [], + ) + for paragraph in predictions + ] + anonymization_paragraph_batch_create_update(paragraph_updates, session=session) + logger.info( + "disambiguation persisted predictions for %d paragraphs", + len(paragraph_updates), + ) + + return DocumentAnnotations( + data=predictions, + label_policies=effective_label_policies if effective_label_policies else None, + ) # MARK: Validate @@ -155,11 +574,21 @@ async def anonymizer_compile_document( """ logger.info(f"receiving => {file.filename}") extension = MIMETYPE_EXTENSION_MAPPER.get(file.content_type) - logger.info(f"detection extension: {extension} ({file.content_type})") + file_suffix = os.path.splitext(file.filename or "")[1].lower() + + if extension is None and file_suffix: + extension = file_suffix.lstrip(".") + + if extension not in {"docx", "pdf"}: + raise HTTPException( + status_code=400, + detail=f"Unsupported format for anonymization: {extension or 'unknown'}", + ) + + logger.info(f"detected extension: {extension} ({file.content_type})") # Create a temporary file - _, suffix = os.path.splitext(file.filename) - suffix = suffix if suffix == ".docx" else ".txt" + suffix = f".{extension}" tmp_dir = tempfile.gettempdir() # Use delete=False to avoid the file being deleted when the NamedTemporaryFile object is closed @@ -178,13 +607,14 @@ async def anonymizer_compile_document( annots_json = json.loads(annotations) annots = DocumentAnnotations.model_validate(annots_json) - logger.info(f"processing annotations => {annots}") + + effective_label_policies = _merge_label_policies(annots.label_policies) + effective_render_policy = _merge_render_policy(annots.render_policy) # Add paragraphs to the database # validation MUST be at least an empty list, to remember user feedback paragraphs = [ - AnonymizationParagraph( - id=text_to_uuid(paragraph.document), + AnonymizationParagraphCreate( text=paragraph.document, validation=paragraph.labels or [], ) @@ -202,35 +632,54 @@ async def anonymizer_compile_document( override=False, ) - # Anonymize the document - doc_anonymizer = DocAnonymizer() + filtered_annotations = [] + for paragraph in annots.data: + filtered_labels = [ + label + for label in (paragraph.labels or []) + if _should_anonymize_label(label, effective_label_policies) + ] + filtered_annotations.append( + DocumentInformation( + document=paragraph.document, + labels=filtered_labels, + ) + ) + + render_context = _build_render_context( + filtered_annotations, effective_render_policy, effective_label_policies + ) + + preds = [ + document_information.model_dump(mode="json", exclude_none=True) + for document_information in filtered_annotations + ] - if suffix == ".docx": - item = {"path": tmp_filename} - doc_anonymizer( - item, - [document_information.model_dump() for document_information in annots.data], + try: + anonymizer = get_anonymizer(extension) + anonymized_path = anonymizer( + {"path": tmp_filename}, + preds, tmp_dir, + render_context=render_context, + ) + except (ValueError, InvalidDocumentAnonymizer) as exc: + if os.path.exists(tmp_filename): + os.remove(tmp_filename) + raise HTTPException(status_code=400, detail=str(exc)) from exc + + if extension == "pdf": + if os.path.exists(tmp_filename): + os.remove(tmp_filename) + + return FileResponse( + anonymized_path, + background=BackgroundTask(os.remove, anonymized_path), + media_type="application/pdf", + filename=f"{os.path.splitext(file.filename)[0]}.pdf", ) - logger.info(f"saved temp file on local storage => {tmp_filename}") - - else: - # Export as raw document - anonymized_doc = [ - doc_anonymizer.replace_labels_in_text(document_information.model_dump()) - .replace("<", "<") - .replace(">", ">") - for document_information in annots.data - ] - with open(tmp_filename, "w") as f: - f.write("\n".join(anonymized_doc)) - - # Add watermark to the end of the document - f.write( - "\n\nDocumento anonimizado por AymurAI\n\nhttps://www.aymurai.info/" - ) - # Convert to ODT + # DOCX flow keeps ODT output cmd = [ settings.LIBREOFFICE_BIN, "--headless", @@ -238,9 +687,8 @@ async def anonymizer_compile_document( "odt", "--outdir", tmp_dir, - tmp_filename, + anonymized_path, ] - logger.info(f"Executing: {' '.join(cmd)}") try: @@ -248,20 +696,20 @@ async def anonymizer_compile_document( cmd, shell=False, encoding="utf-8", errors="ignore" ) logger.info(f"LibreOffice output: {output}") - except subprocess.CalledProcessError as e: + except subprocess.CalledProcessError as exc: raise RuntimeError( - f"LibreOffice conversion failed: {e.output.decode('utf-8', errors='ignore')}" - ) + f"LibreOffice conversion failed: {exc.output.decode('utf-8', errors='ignore')}" + ) from exc + finally: + if os.path.exists(tmp_filename): + os.remove(tmp_filename) - odt = tmp_filename.replace(suffix, ".odt") + odt = f"{os.path.splitext(anonymized_path)[0]}.odt" logger.info(f"Expected output file path: {odt}") if not os.path.exists(odt): raise RuntimeError(f"File at path {odt} does not exist.") - # Ensure the temporary file is deleted - os.remove(tmp_filename) - return FileResponse( odt, background=BackgroundTask(os.remove, odt), diff --git a/aymurai/api/endpoints/routers/datapublic/datapublic.py b/aymurai/api/endpoints/routers/datapublic/datapublic.py index b83ae0d0..4b531390 100644 --- a/aymurai/api/endpoints/routers/datapublic/datapublic.py +++ b/aymurai/api/endpoints/routers/datapublic/datapublic.py @@ -1,17 +1,16 @@ import os from threading import Lock -import torch -from fastapi import Body, Depends, Query, HTTPException +from fastapi import Body, Depends, HTTPException, Query from fastapi.routing import APIRouter from pydantic import UUID5 from sqlmodel import Session from aymurai.api.utils import load_pipeline from aymurai.database.schema import ( - DataPublicParagraph, DataPublicDocument, DataPublicDocumentParagraph, + DataPublicParagraph, ) from aymurai.database.session import get_session from aymurai.database.utils import text_to_uuid @@ -28,7 +27,6 @@ RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH -torch.set_num_threads = 100 # FIXME: polemic ? pipeline_lock = Lock() @@ -62,7 +60,7 @@ async def predict_over_text( logger.info("Running prediction") item = [{"path": "empty", "data": {"doc.text": text_request.text}}] pipeline = load_pipeline( - os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "full-paragraph") + os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "datapublic") ) with pipeline_lock: @@ -72,6 +70,7 @@ async def predict_over_text( text = get_element(processed[0], ["data", "doc.text"]) or "" labels = get_element(processed[0], ["predictions", "entities"]) or [] + paragraph: DataPublicParagraph | None = None if use_cache: logger.info(f"saving in cache: {paragraph_id}") @@ -92,7 +91,9 @@ async def predict_over_text( # paragraph = datapublic_paragraph_create(paragraph, session=session) - return DocumentInformation(document=text, labels=paragraph.prediction) + return DocumentInformation( + document=text, labels=paragraph.prediction if paragraph else labels + ) # MARK: Validate Paragraph diff --git a/aymurai/api/endpoints/routers/frontend/__init__.py b/aymurai/api/endpoints/routers/frontend/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/aymurai/api/endpoints/routers/frontend/__init__.py @@ -0,0 +1 @@ + diff --git a/aymurai/api/endpoints/routers/frontend/frontend.py b/aymurai/api/endpoints/routers/frontend/frontend.py new file mode 100644 index 00000000..22ceaf41 --- /dev/null +++ b/aymurai/api/endpoints/routers/frontend/frontend.py @@ -0,0 +1,114 @@ +from pathlib import Path + +from fastapi import APIRouter, HTTPException +from fastapi.responses import FileResponse, RedirectResponse, Response + +from aymurai.settings import settings + +router = APIRouter(include_in_schema=False) + +LEGACY_API_ROUTE_PREFIXES = ( + "anonymizer", + "api", + "convert", + "datapublic", + "database", + "document-extract", + "docs", + "misc", + "openapi.json", + "redoc", + "server", +) +FEATURE_ROUTE_SLUGS = ("anonymizer", "data_set") +FEATURE_ROUTE_STEPS = ("", "finish", "onboarding", "preview", "process", "validation") +LEGACY_FEATURE_ROUTE_SLUGS = { + "ANONYMIZER": "anonymizer", + "DATA_SET": "data_set", +} + + +def _frontend_dist_dir() -> Path: + return Path(settings.FRONTEND_DIST_DIR).expanduser() + + +def _split_asset_path(asset_path: str) -> list[str]: + return [segment for segment in asset_path.lstrip("/").split("/") if segment] + + +def _is_frontend_feature_route(asset_path: str) -> bool: + segments = _split_asset_path(asset_path) + if not segments or segments[0] not in FEATURE_ROUTE_SLUGS: + return False + + return len(segments) == 1 or segments[1] in FEATURE_ROUTE_STEPS + + +def _is_legacy_api_route(asset_path: str) -> bool: + first_segment = asset_path.lstrip("/").split("/", maxsplit=1)[0] + if _is_frontend_feature_route(asset_path): + return False + return first_segment in LEGACY_API_ROUTE_PREFIXES + + +def _canonical_frontend_path(asset_path: str) -> str | None: + segments = _split_asset_path(asset_path) + if not segments: + return None + + if segments[0] == "app": + canonical_segments = segments[1:] + if canonical_segments: + canonical_feature = LEGACY_FEATURE_ROUTE_SLUGS.get(canonical_segments[0]) + if canonical_feature: + canonical_segments = [canonical_feature, *canonical_segments[1:]] + return "/" + "/".join(canonical_segments) + + canonical_feature = LEGACY_FEATURE_ROUTE_SLUGS.get(segments[0]) + if canonical_feature: + return "/" + "/".join([canonical_feature, *segments[1:]]) + + return None + + +def _resolve_frontend_path(asset_path: str) -> Path: + dist_dir = _frontend_dist_dir().resolve() + requested_path = (dist_dir / asset_path.lstrip("/")).resolve() + requested_path.relative_to(dist_dir) + return requested_path + + +def _serve_frontend(asset_path: str = "") -> Response: + canonical_path = _canonical_frontend_path(asset_path) + if canonical_path is not None: + return RedirectResponse(canonical_path or "/") + + if asset_path and _is_legacy_api_route(asset_path): + raise HTTPException(status_code=404, detail="API route not found.") + + dist_dir = _frontend_dist_dir() + index_file = dist_dir / "index.html" + if not dist_dir.is_dir() or not index_file.is_file(): + raise HTTPException(status_code=404, detail="Frontend build not found.") + + if asset_path: + try: + requested_path = _resolve_frontend_path(asset_path) + except ValueError as error: + raise HTTPException( + status_code=404, detail="Frontend asset not found." + ) from error + + if requested_path.is_file(): + return FileResponse(requested_path) + + if Path(asset_path).suffix: + raise HTTPException(status_code=404, detail="Frontend asset not found.") + + return FileResponse(index_file) + + +@router.get("/") +@router.get("/{asset_path:path}") +async def frontend_app(asset_path: str = "") -> Response: + return _serve_frontend(asset_path) diff --git a/aymurai/api/endpoints/routers/misc/document_extract.py b/aymurai/api/endpoints/routers/misc/document_extract.py index be5d4d63..ab1c61b3 100644 --- a/aymurai/api/endpoints/routers/misc/document_extract.py +++ b/aymurai/api/endpoints/routers/misc/document_extract.py @@ -23,19 +23,26 @@ def extraction(path: str) -> str: """ Wrapper function to call the extract_document function. This is necessary to ensure that the function can be pickled and run in a separate process. + + Args: + path (str): Path to the file to be processed. + + Returns: + str: Extracted text from the document. """ text = extract_document(path) - return document_normalize(text) if text else "" + return document_normalize(text, preserve_paragraphs=True) if text else "" -def run_safe_text_extraction(path: str, timeout_s: float = 30) -> str: +def run_safe_text_extraction(path: str, timeout_s: float | None = 300) -> str: """ Runs the text extraction in a separate process to avoid blocking the main thread. This is useful for long-running tasks or when the extraction might hang. Args: path (str): Path to the file to be processed. - timeout_s (float): Timeout in seconds for the extraction process. Defaults to 30 seconds. + timeout_s (float | None): Timeout in seconds for the extraction process. + If None, waits indefinitely. Defaults to 300. Returns: str: Extracted text from the document. @@ -43,7 +50,6 @@ def run_safe_text_extraction(path: str, timeout_s: float = 30) -> str: Raises: TimeoutError: If the extraction process exceeds the specified timeout. """ - with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor: future = executor.submit(extraction, path) try: @@ -54,8 +60,31 @@ def run_safe_text_extraction(path: str, timeout_s: float = 30) -> str: raise +def _split_document_paragraphs(document: str) -> list[str]: + if re.search(r"\n\s*\n+", document): + raw_paragraphs = re.split(r"\n\s*\n+", document) + else: + raw_paragraphs = document.splitlines() + + paragraphs = [ + re.sub(r"[ \t]{2,}", " ", paragraph.strip()) + for paragraph in raw_paragraphs + if paragraph.strip() + ] + return list(unique_justseen(paragraphs)) + + @router.post("/document-extract", response_model=Document) def plain_text_extractor(file: UploadFile) -> Document: + """ + Extract plain text from an uploaded document. + + Args: + file (UploadFile): Incoming document upload. + + Returns: + Document: Extracted and normalized document payload. + """ logger.info(f"receiving => {file.filename}") extension = MIMETYPE_EXTENSION_MAPPER.get(file.content_type) logger.info(f"detected extension: {extension} ({file.content_type})") @@ -93,9 +122,6 @@ def plain_text_extractor(file: UploadFile) -> Document: logger.info(f"removed temp file from local storage => {tmp_filename}") document_id = data_to_uuid(data) - - paragraphs = [line.strip() for line in document.split("\n") if line.strip()] - paragraphs = [re.sub(r"\s{2,}", " ", line) for line in paragraphs] - paragraphs = list(unique_justseen(paragraphs)) + paragraphs = _split_document_paragraphs(document) return Document(document=paragraphs, document_id=document_id) diff --git a/aymurai/api/main.py b/aymurai/api/main.py index d814a3f6..27ab0c70 100644 --- a/aymurai/api/main.py +++ b/aymurai/api/main.py @@ -2,18 +2,19 @@ import time from contextlib import asynccontextmanager -import torch from alembic import command from alembic.config import Config from fastapi import FastAPI, Request -from fastapi.responses import RedirectResponse from fastapi.middleware.cors import CORSMiddleware +from starlette.formparsers import MultiPartParser from aymurai.api import core +from aymurai.api.endpoints.routers.frontend import frontend +from aymurai.api.startup.database import check_db_connection +from aymurai.api.utils import configure_torch_threads from aymurai.logger import get_logger -from aymurai.settings import settings from aymurai.pipeline import AymurAIPipeline -from aymurai.api.startup.database import check_db_connection +from aymurai.settings import settings try: from aymurai.version import __version__ @@ -23,10 +24,26 @@ logger = get_logger(__name__) -torch.set_num_threads = 100 # FIXME: polemic ? +configure_torch_threads() RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH +MULTIPART_MAX_PART_SIZE = 10 * 1024 * 1024 # 10 MB + + +def _set_kwdefault(func, name: str, value: int) -> None: + kwdefaults = getattr(func, "__kwdefaults__", None) + if kwdefaults and name in kwdefaults: + kwdefaults[name] = value + + +# FastAPI parses Form(...) before endpoint execution. Starlette defaults each +# non-file multipart field to 1MB, which is too small for annotations JSON. +MultiPartParser.max_part_size = MULTIPART_MAX_PART_SIZE +_set_kwdefault(MultiPartParser.__init__, "max_part_size", MULTIPART_MAX_PART_SIZE) +_set_kwdefault(Request.form, "max_part_size", MULTIPART_MAX_PART_SIZE) +_set_kwdefault(Request._get_form, "max_part_size", MULTIPART_MAX_PART_SIZE) + @asynccontextmanager async def lifespan(app: FastAPI): @@ -37,8 +54,9 @@ async def lifespan(app: FastAPI): logger.info(">> Running Alembic migrations") alembic_cfg = Config(str(settings.ALEMBIC_INI_PATH)) command.upgrade(alembic_cfg, "head") - except Exception as error: - logger.error("Error while starting up:", error) + except Exception: + logger.exception("Error while starting up") + raise yield @@ -47,6 +65,9 @@ async def lifespan(app: FastAPI): title="AymurAI API", version=__version__, lifespan=lifespan, + docs_url="/api/docs", + redoc_url="/api/redoc", + openapi_url="/api/openapi.json", ) @@ -73,33 +94,20 @@ async def add_process_time_header(request: Request, call_next): return response -@api.get("/", response_class=RedirectResponse, include_in_schema=False) -async def index(): - return "/docs" - - -# @api.get("/docs", include_in_schema=False) -# async def custom_swagger_ui_html(): -# return get_swagger_ui_html( -# openapi_url=api.openapi_url, -# title=f"{api.title} - Swagger UI", -# swagger_css_url="https://cdn.jsdelivr.net/gh/danielperezrubio/swagger-dark-theme@main/assets/swagger-ui.min.css", -# ) - - ################################################################################ # MARK: API ENDPOINTS ################################################################################ # Healthcheck -@api.get("/server/healthcheck", status_code=200, tags=["server"]) +@api.get("/api/server/healthcheck", status_code=200, tags=["server"]) def healthcheck(): return {"status": "ok"} # Api endpoints -api.include_router(core.router) +api.include_router(core.router, prefix="/api") +api.include_router(frontend.router) if __name__ == "__main__": @@ -109,5 +117,5 @@ def healthcheck(): os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "flair-anonymizer") ) AymurAIPipeline.load( - os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "full-paragraph") + os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "datapublic") ) diff --git a/aymurai/api/utils.py b/aymurai/api/utils.py index 0132eaac..1944c9d7 100644 --- a/aymurai/api/utils.py +++ b/aymurai/api/utils.py @@ -1,4 +1,5 @@ import cachetools +import torch from aymurai.logger import get_logger from aymurai.settings import settings @@ -9,7 +10,7 @@ mem_cache = cachetools.TTLCache( maxsize=settings.MEMORY_CACHE_MAXSIZE, - ttl=settings.MEMORY_CACHE_MAXSIZE, + ttl=settings.MEMORY_CACHE_TTL, getsizeof=lambda _: 0, ) @@ -19,3 +20,7 @@ @cachetools.cached(cache=mem_cache) def load_pipeline(path: str): return AymurAIPipeline.load(path) + + +def configure_torch_threads() -> None: + torch.set_num_threads(settings.TORCH_NUM_THREADS) diff --git a/aymurai/models/peft/__init__.py b/aymurai/config/__init__.py similarity index 100% rename from aymurai/models/peft/__init__.py rename to aymurai/config/__init__.py diff --git a/aymurai/config/default_disambiguation_label_policies.yml b/aymurai/config/default_disambiguation_label_policies.yml new file mode 100644 index 00000000..4de95df8 --- /dev/null +++ b/aymurai/config/default_disambiguation_label_policies.yml @@ -0,0 +1,124 @@ +PER: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: true + +NUM_EXPEDIENTE: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +CUIJ: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +NUM_ACTUACION: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +DNI: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +LINK: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +TELEFONO: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +FECHA: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +DIRECCION: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +TEXTO_ANONIMIZAR: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +LOC: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +CORREO_ELECTRONICO: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +CUIT_CUIL: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +NACIONALIDAD: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +EDAD: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +NOMBRE_ARCHIVO: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +BANCO: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +NUM_MATRICULA: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +ESTUDIOS: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +IP: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +USUARIX: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +CBU: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +MARCA_AUTOMOVIL: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +PATENTE_DOMINIO: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +NUM_CAJA_AHORRO: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false diff --git a/aymurai/database/crud/anonymization/paragraph.py b/aymurai/database/crud/anonymization/paragraph.py index 593ebbdc..cf1e30e5 100644 --- a/aymurai/database/crud/anonymization/paragraph.py +++ b/aymurai/database/crud/anonymization/paragraph.py @@ -1,5 +1,7 @@ +import json import uuid +from pydantic import TypeAdapter from sqlmodel import Session from aymurai.database.schema import ( @@ -8,9 +10,54 @@ AnonymizationParagraphUpdate, ) from aymurai.database.utils import text_to_uuid -from aymurai.logger import get_logger +from aymurai.meta.api_interfaces import DocLabel -logger = get_logger(__name__) +_DOC_LABELS_ADAPTER = TypeAdapter(list[DocLabel]) + + +def _serialize_doclabels(value: list[DocLabel] | None): + """ + Serializes DocLabel objects into JSON-compatible data structures. + + Args: + value (list[DocLabel] | None): DocLabel list to serialize. + + Returns: + list[dict] | None: JSON-safe list of labels, or None if input is None. + """ + if value is None: + return None + + labels = _DOC_LABELS_ADAPTER.dump_python(value, mode="json", exclude_none=True) + deduped = [] + seen = set() + + for label in labels: + key = json.dumps(label, sort_keys=True, separators=(",", ":")) + if key in seen: + continue + + seen.add(key) + deduped.append(label) + + return deduped + + +def _normalize_paragraph_payload(payload: dict) -> dict: + """ + Normalizes paragraph payload fields for JSON storage. + + Args: + payload (dict): Paragraph payload possibly containing DocLabel objects. + + Returns: + dict: Payload with JSON-serializable prediction/validation fields. + """ + if "prediction" in payload: + payload["prediction"] = _serialize_doclabels(payload.get("prediction")) + if "validation" in payload: + payload["validation"] = _serialize_doclabels(payload.get("validation")) + return payload def anonymization_paragraph_create( @@ -18,7 +65,19 @@ def anonymization_paragraph_create( session: Session, override: bool = False, ) -> AnonymizationParagraph: - new_paragraph = AnonymizationParagraph(**paragraph_in.model_dump()) + """ + Creates a new anonymization paragraph record. + + Args: + paragraph_in (AnonymizationParagraphCreate): Paragraph creation payload. + session (Session): Database session. + override (bool): If True, delete any existing paragraph with the same ID. + + Returns: + AnonymizationParagraph: The persisted paragraph record. + """ + payload = _normalize_paragraph_payload(paragraph_in.model_dump(exclude_none=True)) + new_paragraph = AnonymizationParagraph(**payload) if override: existing = session.get(AnonymizationParagraph, new_paragraph.id) @@ -36,6 +95,16 @@ def anonymization_paragraph_read( paragraph_id: uuid.UUID, session: Session, ) -> AnonymizationParagraph | None: + """ + Reads a paragraph record by ID. + + Args: + paragraph_id (uuid.UUID): Paragraph UUID. + session (Session): Database session. + + Returns: + AnonymizationParagraph | None: Paragraph record if found. + """ return session.get(AnonymizationParagraph, paragraph_id) @@ -44,12 +113,26 @@ def anonymization_paragraph_update( paragraph_in: AnonymizationParagraphUpdate, session: Session, ) -> AnonymizationParagraph: + """ + Updates an existing paragraph record. + + Args: + paragraph_id (uuid.UUID): Paragraph UUID to update. + paragraph_in (AnonymizationParagraphUpdate): Update payload. + session (Session): Database session. + + Returns: + AnonymizationParagraph: The updated paragraph record. + """ paragraph = session.get(AnonymizationParagraph, paragraph_id) if not paragraph: raise ValueError(f"Paragraph not found: {paragraph_id}") - for field, value in paragraph_in.model_dump(exclude_none=True).items(): + payload = _normalize_paragraph_payload( + paragraph_in.model_dump(exclude_none=True, mode="json") + ) + for field, value in payload.items(): setattr(paragraph, field, value) session.add(paragraph) @@ -59,6 +142,16 @@ def anonymization_paragraph_update( def anonymization_paragraph_delete(paragraph_id: uuid.UUID, session: Session): + """ + Deletes a paragraph record by ID. + + Args: + paragraph_id (uuid.UUID): Paragraph UUID to delete. + session (Session): Database session. + + Returns: + None + """ paragraph = session.get(AnonymizationParagraph, paragraph_id) if not paragraph: @@ -74,6 +167,16 @@ def anonymization_paragraph_delete(paragraph_id: uuid.UUID, session: Session): def anonymization_paragraph_batch_create_update( paragraphs_in: list[AnonymizationParagraphCreate], session: Session ) -> list[AnonymizationParagraph]: + """ + Creates or updates a batch of paragraph records. + + Args: + paragraphs_in (list[AnonymizationParagraphCreate]): Paragraph payloads. + session (Session): Database session. + + Returns: + list[AnonymizationParagraph]: Persisted paragraph records. + """ paragraphs = [] for p_in in paragraphs_in: @@ -81,13 +184,15 @@ def anonymization_paragraph_batch_create_update( paragraph = session.get(AnonymizationParagraph, paragraph_id) if paragraph: - update = AnonymizationParagraphUpdate(**p_in.model_dump()) - - for field, value in update.model_dump(exclude_none=True).items(): - setattr(paragraph, field, value) + payload = _normalize_paragraph_payload(p_in.model_dump(exclude_none=True)) + payload.pop("id", None) + for field, value in payload.items(): + if value is not None: + setattr(paragraph, field, value) else: - paragraph = AnonymizationParagraph(**p_in.model_dump()) + payload = _normalize_paragraph_payload(p_in.model_dump(exclude_none=True)) + paragraph = AnonymizationParagraph(**payload) session.add(paragraph) session.commit() diff --git a/aymurai/datasets/ar_juz_pcyf_10/labelstudio/utils.py b/aymurai/datasets/ar_juz_pcyf_10/labelstudio/utils.py index 94e896db..4cecf591 100644 --- a/aymurai/datasets/ar_juz_pcyf_10/labelstudio/utils.py +++ b/aymurai/datasets/ar_juz_pcyf_10/labelstudio/utils.py @@ -1,13 +1,13 @@ import re -from glob import glob from copy import deepcopy +from glob import glob from itertools import groupby +from more_itertools import collapse, unzip from numpy import cumsum -from more_itertools import unzip, collapse -from aymurai.meta.types import DataItem from aymurai.meta.entities import Entity +from aymurai.meta.types import DataItem from aymurai.utils.json_data import load_json @@ -43,6 +43,8 @@ def reformat_entity(text: str, span: dict) -> dict: end=span["end"], label=span["labels"][0], text=text[span["start"] : span["end"]], + start_char=span["start"], + end_char=span["end"], context_pre=text[soffset : span["start"]], context_post=text[span["end"] : eoffset], attrs={"aymurai_label": span["labels"][0]}, diff --git a/aymurai/datasets/ar_juz_pcyf_10/public.py b/aymurai/datasets/ar_juz_pcyf_10/public.py index 09b9870e..5c7b3cc6 100644 --- a/aymurai/datasets/ar_juz_pcyf_10/public.py +++ b/aymurai/datasets/ar_juz_pcyf_10/public.py @@ -1,17 +1,17 @@ -import sys import logging -import subprocess +import sys +from collections import UserList +from datetime import date, time from pathlib import Path from typing import Any, Union -from collections import UserList -from datetime import date, time, datetime import datasets import pandas as pd -from aymurai.text.extraction import get_extension from aymurai.datasets.ar_juz_pcyf_10.common import BASE, FIELDS +from aymurai.text.extraction import get_extension from aymurai.utils.cache import cache_load, cache_save, get_cache_key +from aymurai.utils.download import download logging.basicConfig(stream=sys.stdout, level=logging.INFO) logger = logging.getLogger(__name__) @@ -38,10 +38,7 @@ def to_datetime(value): @staticmethod def get_file(source: str, dest: str) -> str: - # gdown have to much verbosity and cant be quiet (at least in this version) - # as workaround we use subprocess to get all output (even errors) - cmd = f"gdown --fuzzy -q --continue -O {dest} {source}" - subprocess.getoutput(cmd) + download(source, dest) if not Path(dest).exists(): Path(dest).touch() @@ -118,7 +115,6 @@ def __init__(self, use_cache: Union[str, bool] = True): data = [] GROUPBY_COLUMNS = ["nro_registro", "tomo", "path"] for keys, group in annotations.groupby(GROUPBY_COLUMNS): - data_ = {} data_["path"] = keys[2] data_["metadata"] = { diff --git a/aymurai/models/usem/__init__.py b/aymurai/evaluation/__init__.py similarity index 100% rename from aymurai/models/usem/__init__.py rename to aymurai/evaluation/__init__.py diff --git a/aymurai/evaluation/metrics.py b/aymurai/evaluation/metrics.py new file mode 100644 index 00000000..ced35d99 --- /dev/null +++ b/aymurai/evaluation/metrics.py @@ -0,0 +1,401 @@ +from __future__ import annotations + +import json +import unicodedata +from pathlib import Path +from typing import Any + +from aymurai.logger import get_logger +from aymurai.utils.json_data import load_json + +logger = get_logger(__name__) + + +def normalize_text(text: str) -> str: + """ + Normalize text by lowercasing, stripping, and removing accents. + + Args: + text (str): Input text. + + Returns: + str: Normalized text. + """ + if not text: + return "" + + # Lowercase and strip + text = text.strip().lower() + + # Remove accents + text = unicodedata.normalize("NFD", text) + text = "".join(ch for ch in text if unicodedata.category(ch) != "Mn") + + return text + + +def get_alias_set(entity: dict[str, Any], normalize: bool = True) -> set[str]: + """ + Returns the set of normalized aliases for an entity, ensuring that the + canonical_text is included as an alias. + + Args: + entity (dict[str, Any]): A dictionary containing entity information with + optional 'aliases' (list of strings) and 'canonical_text' (string) keys. + normalize (bool, optional): If True, normalizes the alias texts using + normalize_text function. Defaults to True. + + Returns: + set[str]: A set of alias strings (normalized if normalize=True) including + both explicit aliases and the canonical text. + """ + aliases = set() + + # Get aliases from the entity + for alias in entity.get("aliases", []): + text = normalize_text(alias) if normalize else alias + if text: + aliases.add(text) + + # Get canonical text from the entity + canonical = ( + normalize_text(entity.get("canonical_text", "")) + if normalize + else entity.get("canonical_text", "") + ) + if canonical: + aliases.add(canonical) + + return aliases + + +def alias_jaccard(aliases_gold: set[str], aliases_pred: set[str]) -> float: + """ + Jaccard similarity between two sets of aliases. + + Args: + aliases_gold (set[str]): Set of gold aliases. + aliases_pred (set[str]): Set of predicted aliases. + + Returns: + float: Jaccard similarity score. + """ + # If both sets are empty, define similarity as 1.0 + if not aliases_gold and not aliases_pred: + return 1.0 + + # Calculate Jaccard similarity + union = aliases_gold | aliases_pred + inter = aliases_gold & aliases_pred + + return len(inter) / len(union) + + +def greedy_matching( + sim_matrix: list[list[float]], sim_threshold: float +) -> list[tuple[int, int, float]]: + """ + Perform greedy maximum matching based on similarity scores. + + Args: + sim_matrix (list[list[float]]): Similarity matrix where sim_matrix[i][j] + represents the similarity between the gold entity i and predicted + entity j. + sim_threshold (float): Minimum similarity required to consider a match + between two entities. + + Returns: + list[tuple[int, int, float]]: Tuples containing the matched gold index, + predicted index, and their similarity score. + """ + matches: list[tuple[int, int, float]] = [] + num_gold = len(sim_matrix) + num_pred = len(sim_matrix[0]) if num_gold > 0 else 0 + + # Build the list of candidate matches above the threshold + candidates: list[tuple[float, int, int]] = [] + for i in range(num_gold): + for j in range(num_pred): + sim = sim_matrix[i][j] + if sim >= sim_threshold: + candidates.append((sim, i, j)) + + # Sort descending by similarity so higher matches are selected first + candidates.sort(reverse=True, key=lambda x: x[0]) + + used_gold = set() + used_pred = set() + + for sim, i, j in candidates: + if i in used_gold or j in used_pred: + continue + used_gold.add(i) + used_pred.add(j) + matches.append((i, j, sim)) + + return matches + + +def compute_metrics_components( + gold_entities: list[dict[str, Any]], + pred_entities: list[dict[str, Any]], + sim_threshold: float = 0.3, + normalize: bool = True, + target_label: str = None, +) -> dict[str, float]: + """ + Compute each component of the disambiguation metric by label if provided. + + The metric includes entity-level F1, macro average alias F1, and accuracy + for labels and roles on matched entities. + + Args: + gold_entities (list[dict[str, Any]]): Ground-truth entities. + pred_entities (list[dict[str, Any]]): Predicted entities. + sim_threshold (float): Minimum alias similarity to consider entities a match. + Defaults to 0.3. + normalize (bool): If True normalizes aliases before comparison. + Defaults to True. + + Returns: + dict[str, float]: Mapping with the keys F1_ent, AliasF1_macro, Acc_label, and Acc_role. + """ + # Filter entities by target label if provided + if target_label: + gold_entities = [ + e for e in gold_entities if e.get("aymurai_label") == target_label + ] + pred_entities = [ + e for e in pred_entities if e.get("aymurai_label") == target_label + ] + + # Handle the trivial case where both sets are empty + if not gold_entities and not pred_entities: + return { + "F1_ent": 1.0, + "AliasF1_macro": 1.0, + "Acc_label": 1.0, + "Acc_role": 1.0, + } + + # Preprocess aliases, labels and roles + gold_aliases = [get_alias_set(e, normalize=normalize) for e in gold_entities] + pred_aliases = [get_alias_set(e, normalize=normalize) for e in pred_entities] + + gold_labels = [e.get("aymurai_label") for e in gold_entities] + pred_labels = [e.get("aymurai_label") for e in pred_entities] + + gold_roles = [e.get("attributes", {}).get("role") for e in gold_entities] + pred_roles = [e.get("attributes", {}).get("role") for e in pred_entities] + + num_gold = len(gold_entities) + num_pred = len(pred_entities) + + # Build the alias similarity matrix + sim_matrix = [] + for i in range(num_gold): + row = [] + for j in range(num_pred): + sim = alias_jaccard(gold_aliases[i], pred_aliases[j]) + row.append(sim) + sim_matrix.append(row) + + # Perform greedy matching + matches = greedy_matching(sim_matrix, sim_threshold=sim_threshold) + + TP = len(matches) + FN = num_gold - TP + FP = num_pred - TP + + # Entity-level precision/recall/F1 + if TP + FP > 0: + P_ent = TP / (TP + FP) + else: + P_ent = 0.0 + + if TP + FN > 0: + R_ent = TP / (TP + FN) + else: + R_ent = 0.0 + + if P_ent + R_ent > 0: + F1_ent = 2 * P_ent * R_ent / (P_ent + R_ent) + else: + F1_ent = 0.0 + + # Alias-level F1 macro + if TP > 0: + alias_f1_sum = 0.0 + label_correct = 0 + role_correct = 0 + + for i, j, _sim in matches: + A_g = gold_aliases[i] + A_p = pred_aliases[j] + + inter = len(A_g & A_p) + P_alias = inter / len(A_p) if len(A_p) > 0 else 0.0 + R_alias = inter / len(A_g) if len(A_g) > 0 else 0.0 + if P_alias + R_alias > 0: + F1_alias = 2 * P_alias * R_alias / (P_alias + R_alias) + else: + F1_alias = 0.0 + + alias_f1_sum += F1_alias + + # Label accuracy on matched entities + if gold_labels[i] == pred_labels[j]: + label_correct += 1 + + # Role accuracy on matched entities + if gold_roles[i] == pred_roles[j]: + role_correct += 1 + + AliasF1_macro = alias_f1_sum / TP + Acc_label = label_correct / TP + Acc_role = role_correct / TP + else: + AliasF1_macro = 0.0 + Acc_label = 0.0 + Acc_role = 0.0 + + return { + "F1_ent": F1_ent, + "AliasF1_macro": AliasF1_macro, + "Acc_label": Acc_label, + "Acc_role": Acc_role, + } + + +def evaluate_disambiguation( + gold_json: Any, + pred_json: Any, + w_ent: float = 0.4, + w_alias: float = 0.35, + w_label: float = 0.2, + w_role: float = 0.05, + sim_threshold: float = 0.3, + normalize: bool = True, + target_label: str = None, +) -> tuple[float, dict[str, float]]: + """ + Evaluate disambiguation predictions against ground truth entities by label if provided. + + Args: + gold_json (Any): Ground-truth entities as a parsed list or JSON string. + pred_json (Any): Predicted entities as a parsed list or JSON string. + w_ent (float): Weight assigned to entity-level F1. Defaults to 0.4. + w_alias (float): Weight assigned to alias macro F1. Defaults to 0.35. + w_label (float): Weight assigned to label accuracy. Defaults to 0.2. + w_role (float): Weight assigned to role accuracy. Defaults to 0.05. + sim_threshold (float): Minimum alias similarity to consider entities a match. + Defaults to 0.3. + normalize (bool): If True normalizes aliases before comparison. + Defaults to True. + + Returns: + tuple[float, dict[str, float]]: Overall disambiguation score and detailed metrics components. + """ + + # Parse JSON strings if necessary + if isinstance(gold_json, str): + gold_entities = json.loads(gold_json) + else: + gold_entities = gold_json + + if isinstance(pred_json, str): + pred_entities = json.loads(pred_json) + else: + pred_entities = pred_json + + metrics = compute_metrics_components( + gold_entities, + pred_entities, + sim_threshold=sim_threshold, + normalize=normalize, + target_label=target_label, + ) + + score = ( + w_ent * metrics["F1_ent"] + + w_alias * metrics["AliasF1_macro"] + + w_label * metrics["Acc_label"] + + w_role * metrics["Acc_role"] + ) + + return score, metrics + + +def evaluate_prediction_directories( + gold_dir: Path, + preds_dir: Path, + *, + sim_threshold: float = 0.3, + normalize: bool = True, + target_label: str = None, + gold_json_suffix: str = None, + pred_json_suffix: str = None, +) -> tuple[list[tuple[str, float, dict[str, float]]], float]: + """ + Evaluate predictions stored on disk against the gold standard set by label if provided. + It has the feature of comparing files in two directories based on their core names, + ignoring predefined suffixes. + + Args: + gold_dir (Path): Directory containing gold json files. + preds_dir (Path): Directory containing predicted json files. + sim_threshold (float): Minimum alias similarity to consider entities a match. + Defaults to 0.3. + normalize (bool): If True normalizes aliases before comparison. + Defaults to True. + target_label (str, optional): If provided, evaluates only entities with this label. + Defaults to None. + gold_json_suffix (str, optional): Suffix to remove from gold json filenames + when matching with predictions. Defaults to None. + pred_json_suffix (str, optional): Suffix to remove from predicted json filenames + when matching with gold files. Defaults to None. + + Returns: + tuple[list[tuple[str, float, dict[str, float]]], float]: Detailed results + per document and the average score across all evaluated documents. + """ + gold_dir = Path(gold_dir) + preds_dir = Path(preds_dir) + + results = [] + + pred_map = {} + for p in preds_dir.glob("*.json"): + core_name = p.stem.replace(pred_json_suffix, "") + pred_map[core_name] = p + + for gold_path in sorted(gold_dir.glob("*.json")): + core_id = gold_path.stem.replace(gold_json_suffix, "") + pred_path = pred_map.get(core_id) + + if not pred_path or not pred_path.exists(): + logger.warning( + f"Skipping: No prediction found for '{core_id}'. " + f"Expected something like '{core_id}{pred_json_suffix}.json'" + ) + continue + + gold_data = load_json(gold_path) + pred_data = load_json(pred_path) + + score, metrics = evaluate_disambiguation( + gold_data, + pred_data, + sim_threshold=sim_threshold, + normalize=normalize, + target_label=target_label, + ) + results.append((core_id, score, metrics)) + + average_score = ( + sum(score for _, score, _ in results) / len(results) + if results + else float("nan") + ) + + return results, average_score diff --git a/aymurai/meta/api_interfaces.py b/aymurai/meta/api_interfaces.py index 1ff51443..ab1e8845 100644 --- a/aymurai/meta/api_interfaces.py +++ b/aymurai/meta/api_interfaces.py @@ -1,4 +1,5 @@ import uuid +from typing import Literal from pydantic import UUID5, BaseModel, Field, RootModel @@ -42,10 +43,27 @@ class DocumentInformation(BaseModel): labels: list[DocLabel] = Field(default_factory=list) +class LabelPolicy(BaseModel): + """Per-label policy for disambiguation and anonymization.""" + + anonymize: bool | None = None + disambiguation: Literal["none", "fuzzy"] | None = None + use_subclass_when_available: bool | None = None + + +class RenderPolicy(BaseModel): + """Render policy for anonymized tokens.""" + + suffix_mode: Literal["auto", "always", "never"] | None = None + suffix_threshold: int | None = None + + class DocumentAnnotations(BaseModel): """Datatype for document annotations""" data: list[DocumentInformation] + label_policies: dict[str, LabelPolicy] | None = None + render_policy: RenderPolicy | None = None class DataPublicDocumentAnnotations(RootModel): diff --git a/aymurai/meta/entities.py b/aymurai/meta/entities.py index d16c0b68..fe36b762 100644 --- a/aymurai/meta/entities.py +++ b/aymurai/meta/entities.py @@ -1,4 +1,10 @@ -from pydantic import BaseModel, Field +from __future__ import annotations + +import json +from typing import Any +from uuid import NAMESPACE_URL, UUID, uuid5 + +from pydantic import BaseModel, Field, model_validator class EntityAttributes(BaseModel): @@ -26,6 +32,21 @@ class EntityAttributes(BaseModel): description="Method used on the prediction label", ) aymurai_score: float | None = Field(None, description="Score for prediction") + aymurai_label_instance: int | None = Field( + None, + description="Label instance index assigned by order of appearance (e.g., 1, 2, 3).", + ) + aymurai_disambiguation: str | None = Field( + None, + description="Override disambiguation mode for this entity (none, fuzzy).", + ) + aymurai_anonymize: bool | None = Field( + None, + description="Whether this entity should be anonymized in output.", + ) + canonical_entity_id: UUID | None = Field( + None, description="Reference to the canonical entity ID" + ) class Entity(BaseModel): @@ -38,3 +59,49 @@ class Entity(BaseModel): context_pre: str = "" context_post: str = "" attrs: EntityAttributes | None = None + + +class CanonicalEntity(BaseModel): + """Canonical representation of an entity cluster.""" + + entity_id: UUID | None = Field( + None, description="Unique identifier for the canonical entity" + ) + aymurai_label: str = Field(title="AymurAI label") + canonical_text: str = Field(description="Preferred textual form for the entity") + aliases: list[str] = Field( + default_factory=list, + description="Alternative surface forms observed for the entity", + ) + attributes: dict[str, Any] = Field( + default_factory=dict, description="Additional metadata for the entity" + ) + + @model_validator(mode="after") + def validate_entity_id(self) -> CanonicalEntity: + """ + Ensure the canonical entity has an entity_id; if missing, create a surrogate UUID and set it. + + Returns: + CanonicalEntity: the validated model instance (self). + """ + # If entity_id is already set, nothing to do. + if self.entity_id is not None: + return self + + payload = { + "aymurai_label": self.aymurai_label, + "canonical_text": self.canonical_text, + "aliases": sorted(self.aliases), + "attributes": self.attributes, + } + seed = json.dumps(payload, sort_keys=True, separators=(",", ":")) + object.__setattr__(self, "entity_id", uuid5(NAMESPACE_URL, seed)) + + return self + + +class CanonicalEntities(BaseModel): + """Collection of canonical entities.""" + + canonical_entities: list[CanonicalEntity] diff --git a/aymurai/models/decision/binregex.py b/aymurai/models/decision/binregex.py index a3b77a01..bc67e2be 100644 --- a/aymurai/models/decision/binregex.py +++ b/aymurai/models/decision/binregex.py @@ -1,96 +1,114 @@ +from __future__ import annotations + import os -import shutil from copy import deepcopy import regex import torch -from unidecode import unidecode from aymurai.logger import get_logger from aymurai.meta.api_interfaces import DocLabel, EntityAttributes from aymurai.meta.pipeline_interfaces import TrainModule from aymurai.meta.types import DataBlock, DataItem -from aymurai.models.decision.conv1d import Conv1dTextClassifier -from aymurai.models.decision.tokenizer import Tokenizer +from aymurai.models.decision.embeddingbag import ( + TinyEmbeddingBagClassifier, + encode_text, + make_offsets, +) +from aymurai.settings import settings from aymurai.utils.download import download from aymurai.utils.misc import get_element, is_url logger = get_logger(__name__) -class DecisionConv1dBinRegex(TrainModule): +class DecisionEmbeddingBagBinRegex(TrainModule): def __init__( self, - tokenizer_path: str, model_checkpoint: str, device: str = "cpu", threshold: float = 0.88, return_only_with_detalle: bool = True, ): self._device = device - self._tokenizer_path = tokenizer_path self._model_path = model_checkpoint self.threshold = threshold self.return_only_with_detalle = return_only_with_detalle - # download if needed - # tokenizer - basepath = os.getenv("AYMURAI_CACHE_BASEPATH", "/resources/cache/aymurai") - if is_url(url := self._tokenizer_path): - output = f"{basepath}/{self.__name__}/tokenizer.pth" - logger.info(f"downloading tokenizer on {output}") - os.makedirs(os.path.dirname(output), exist_ok=True) - self._tokenizer_path = download(url, output=output) - # model + basepath = settings.CACHE_BASEPATH if is_url(url := self._model_path): - output = f"{basepath}/{self.__name__}/model.ckpt" + # Determine file extension from URL or default to safetensors + if url.endswith(".pt"): + ext = ".pt" + else: + ext = ".safetensors" + output = f"{basepath}/{self.__class__.__name__}/model{ext}" logger.info(f"downloading model on {output}") os.makedirs(os.path.dirname(output), exist_ok=True) self._model_path = download(url, output=output) - - self.tokenizer = Tokenizer.load(self._tokenizer_path) - self.model = Conv1dTextClassifier.load_from_checkpoint( + # If downloading safetensors, also try to download config + if ext == ".safetensors": + config_url = url.rsplit(".safetensors", 1)[0] + ".json" + config_output = output.rsplit(".safetensors", 1)[0] + ".json" + try: + download(config_url, output=config_output) + except Exception as e: + logger.warning(f"Could not download config file: {e}") + + self.model, self.cfg = TinyEmbeddingBagClassifier.from_checkpoint( self._model_path, - map_location=self._device, + device=self._device, ) self.model = self.model.eval() - def save(self, basepath: str) -> dict | None: - # save tokenizer - os.makedirs(basepath, exist_ok=True) - self._tokenizer_path = f"{basepath}/tokenizer.pth" - self.tokenizer.save(self._tokenizer_path) - logger.info(f"tokenizer saved on: {self._tokenizer_path}") - - # save model - new_model_path = f"{basepath}/model.ckpt" - shutil.copy(self._model_path, new_model_path) - self._model_path = new_model_path - logger.info(f"model saved on: {self._model_path}") - return { - "tokenizer_path": self._tokenizer_path, - "model_checkpoint": self._model_path, - "device": self._device, - } - - @classmethod - def load(cls, path: str, **kwargs): - return cls( - tokenizer_path=f"{path}/tokenizer.pth", - model_checkpoint=f"{path}/model.ckpt", - **kwargs, - ) + def fit(self, train: DataBlock, val: DataBlock) -> None: + """ + Fit the model on training data. Currently not implemented. - def fit(self, train: DataBlock, val: DataBlock): + Args: + train (DataBlock): Training data block. + val (DataBlock): Validation data block. + """ logger.warning("fit routine not implemented") pass def predict(self, data: DataBlock) -> DataBlock: + """ + Predict on a data block. + + Args: + data (DataBlock): Input data block. + + Returns: + DataBlock: Predicted data block. + """ # FIXME: optimize - logger.warn("predict not optimized") + logger.warning("predict not optimized") return [self.predict_single(item) for item in data] - def get_subcategory(self, text): + def model_input_from_text(self, text: str) -> tuple[torch.Tensor, torch.Tensor]: + """ + Convert text to model input tensors. + + Args: + text (str): Input text. + + Returns: + tuple[torch.Tensor, torch.Tensor]: (flat_tokens, offsets) for model input. + """ + token_ids = encode_text(text, self.cfg).to(self._device) + return make_offsets([token_ids]) + + def get_subcategory(self, text: str) -> list[str]: + """ + Determine the subcategory of a decision based on its text. + + Args: + text (str): Text of the decision. + + Returns: + list[str]: List of subcategories for the decision. + """ pattern_no_hace_lugar = regex.compile( r"(?i)(no hacer? lugar|rechaz[ao]r?|no admitir|no convalidar|no autorizar|declarar inadmisible)" ) @@ -100,8 +118,19 @@ def get_subcategory(self, text): else: return ["hace_lugar"] - def gen_aymurai_entity(self, text: str, category: int, score: float): + def gen_aymurai_entity(self, text: str, score: float) -> dict: + """ + Generate an Aymurai entity dictionary for a decision. + + Args: + text (str): Text of the decision. + score (float): Confidence score of the decision. + + Returns: + dict: Aymurai entity dictionary. + """ subcategory = self.get_subcategory(text) + attrs = EntityAttributes( aymurai_label="DECISION", aymurai_label_subclass=subcategory, @@ -119,18 +148,27 @@ def gen_aymurai_entity(self, text: str, category: int, score: float): ent["label"] = "DECISION" ent["context_pre"] = "" ent["context_post"] = "" + return ent def predict_single(self, item: DataItem) -> DataItem: + """ + Predict a single data item. + + Args: + item (DataItem): The data item to predict. + + Returns: + DataItem: The predicted data item with added entities if applicable. + """ item = deepcopy(item) text = item["data"]["doc.text"] - text = unidecode(text) - input_ids = self.tokenizer.encode_batch([text]).to(self.model.device) + flat_tokens, offsets = self.model_input_from_text(text) with torch.no_grad(): - log_prob = self.model(input_ids).exp() - # using category 1 as global score (binary) - prob = log_prob.detach().numpy()[0, 1] + logits = self.model(flat_tokens, offsets) + probs = logits.softmax(dim=1).cpu() + prob = float(probs[0, 1]) category = int(prob > self.threshold) score = prob @@ -143,7 +181,7 @@ def predict_single(self, item: DataItem) -> DataItem: if self.return_only_with_detalle and not detalles: return item - ent = self.gen_aymurai_entity(text=text, category=category, score=score) + ent = self.gen_aymurai_entity(text=text, score=score) ents.append(ent) if "predictions" not in item: @@ -152,3 +190,53 @@ def predict_single(self, item: DataItem) -> DataItem: item["predictions"]["entities"] = ents return item + + def save(self, basepath: str) -> dict: + """ + Save the model to a directory. + + Args: + basepath (str): Directory path to save the model. + + Returns: + dict: A dictionary containing metadata about the saved model. + """ + os.makedirs(basepath, exist_ok=True) + + # Use safetensors as the main format + new_model_path = f"{basepath}/model.safetensors" + self.model.save_checkpoint(new_model_path, use_safetensors=True) + self._model_path = new_model_path + logger.info(f"model saved on: {self._model_path}") + + return { + "model_checkpoint": self._model_path, + "device": self._device, + "threshold": self.threshold, + "return_only_with_detalle": self.return_only_with_detalle, + } + + @classmethod + def load(cls, path: str, **kwargs) -> DecisionEmbeddingBagBinRegex: + """ + Load a DecisionEmbeddingBagBinRegex model from a directory. + + Args: + path (str): Path to the directory containing the model files. + + Returns: + DecisionEmbeddingBagBinRegex: The loaded model instance. + """ + # Try safetensors first, then .pt + safetensors_path = f"{path}/model.safetensors" + pt_path = f"{path}/model.pt" + + if os.path.exists(safetensors_path): + model_checkpoint = safetensors_path + elif os.path.exists(pt_path): + model_checkpoint = pt_path + else: + # Fallback to .pt for backward compatibility + model_checkpoint = pt_path + + return cls(model_checkpoint=model_checkpoint, **kwargs) diff --git a/aymurai/models/decision/conv1d.py b/aymurai/models/decision/conv1d.py deleted file mode 100644 index 9c93dfad..00000000 --- a/aymurai/models/decision/conv1d.py +++ /dev/null @@ -1,138 +0,0 @@ -import pytorch_lightning as pl -import torch -import torch.nn.functional as F -import torchmetrics -from torch import nn -from torch.optim.lr_scheduler import ReduceLROnPlateau - - -class Conv1dTextClassifier(pl.LightningModule): - def __init__( - self, - vocab_size: int, - max_tokens: int = 128, - embed_len: int = 128, - nfeatures: int = 64, - num_classes: int = 2, - lr_scheduler_patience: int = 2, - class_weights: list = [], - ): - self.vocab_size = vocab_size - self.max_tokens = max_tokens - self.embed_len = embed_len - self.nfeatures = nfeatures - self.num_classes = num_classes - self.lr = 1e-3 - self.lr_scheduler_patience = lr_scheduler_patience - self.class_weights = class_weights - - super().__init__() - self.save_hyperparameters() - - # layers - self.embedding_layer = nn.Embedding( - num_embeddings=self.vocab_size, - embedding_dim=self.embed_len, - ) - self.conv1 = nn.Conv1d(self.embed_len, 64, kernel_size=7, padding="same") - self.conv2 = nn.Conv1d(64, 32, kernel_size=7, padding="same") - self.pooling = nn.MaxPool1d(2) - - self.linear1 = nn.Linear(32, 32) - self.linear2 = nn.Linear(32, self.num_classes) - # self.linear = nn.Linear(self.nfeatures, self.num_classes) - - if len(self.class_weights): - self.class_weights = torch.tensor(class_weights, dtype=torch.float32) - - self.logsoftmax = nn.LogSoftmax(dim=1) - self.loss = nn.NLLLoss(weight=self.class_weights) - - # metrics - self.accuracy = torchmetrics.Accuracy( - task="multiclass", - num_classes=self.num_classes, - ) - self.f1score = torchmetrics.F1Score( - task="multiclass", - num_classes=self.num_classes, - ) - - def forward(self, X_batch): - x = self.embedding_layer(X_batch) - x = x.reshape( - len(x), self.embed_len, self.max_tokens - ) ## Embedding Length needs to be treated as channel dimension - x = F.relu(self.conv1(x)) - x = self.pooling(x) - # x = F.dropout(x, 0.5) - x = F.relu(self.conv2(x)) - x, _ = x.max(dim=-1) - - x = self.linear2(x) - x = self.logsoftmax(x) - - return x - - def predict_step(self, batch, batch_idx): - return self(batch) - - def training_step(self, batch, batch_idx): - # training_step defines the train loop. - x, y = batch - - y_pred = self.forward(x) - - loss = self.loss(y_pred, y) - acc = self.accuracy(y_pred, y) - f1score = self.f1score(y_pred, y) - - self.log("loss", loss, on_epoch=True, prog_bar=True, logger=True) - self.log("acc", acc, on_epoch=True, prog_bar=True, logger=True) - self.log("f1score", f1score, on_epoch=True, prog_bar=True, logger=True) - return loss - - def validation_step(self, batch, batch_idx): - x, y = batch - - y_pred = self.forward(x) - - # loss = F.cross_entropy(y_pred, y) - loss = self.loss(y_pred, y) - acc = self.accuracy(y_pred, y) - f1score = self.f1score(y_pred, y) - - self.log("val_loss", loss, on_epoch=True, prog_bar=True, logger=True) - self.log("val_acc", acc, on_epoch=True, prog_bar=True, logger=True) - self.log("val_f1score", f1score, on_epoch=True, prog_bar=True, logger=True) - - def test_step(self, batch, batch_idx): - x, y = batch - - y_pred = self.forward(x) - - # loss = F.cross_entropy(y_pred, y) - loss = self.loss(y_pred, y) - acc = self.accuracy(y_pred, y) - f1score = self.f1score(y_pred, y) - - self.log("test_loss", loss, on_epoch=True, prog_bar=True, logger=True) - self.log("test_acc", acc, on_epoch=True, prog_bar=True, logger=True) - self.log("test_f1score", f1score, on_epoch=True, prog_bar=True, logger=True) - - def configure_optimizers(self): - # optimizer = torch.optim.Adam(self.parameters(), lr=1e-3) - optimizer = torch.optim.AdamW(self.parameters(), lr=self.lr) - return { - "optimizer": optimizer, - "lr_scheduler": { - "scheduler": ReduceLROnPlateau( - optimizer, - patience=self.lr_scheduler_patience, - ), - "monitor": "val_loss", - "frequency": 1, - # If "monitor" references validation metrics, then "frequency" should be set to a - # multiple of "trainer.check_val_every_n_epoch". - }, - } diff --git a/aymurai/models/decision/embeddingbag.py b/aymurai/models/decision/embeddingbag.py new file mode 100644 index 00000000..19e29d46 --- /dev/null +++ b/aymurai/models/decision/embeddingbag.py @@ -0,0 +1,267 @@ +from __future__ import annotations + +import hashlib +import json +import os +import re +from dataclasses import dataclass, field + +import torch +from safetensors.torch import load_file as safe_load_file +from safetensors.torch import save_file as safe_save_file +from torch import nn +from unidecode import unidecode + + +def _normalize_text(text: str) -> str: + """ + Normalize text by removing accents, converting to lowercase, and collapsing whitespace. + + Args: + text (str): The input text to normalize. + + Returns: + str: The normalized text. + """ + text = unidecode(str(text)).lower() + text = re.sub(r"\s+", " ", text).strip() + return text + + +def _tokenize(text: str) -> list[str]: + """ + Tokenize text by splitting on spaces after normalization. + + Args: + text (str): The input text to tokenize. + + Returns: + list[str]: The list of tokens. + """ + text = _normalize_text(text) + return [token for token in text.split(" ") if token] + + +def _hash_token(token: str, vocab_size: int) -> int: + """ + Hash a token string into an integer in the range [0, vocab_size). + + Args: + token (str): The token string to hash. + vocab_size (int): The size of the vocabulary. + + Returns: + int: The hashed token id. + """ + digest = hashlib.blake2b(token.encode("utf-8"), digest_size=4).digest() + return int.from_bytes(digest, "little") % vocab_size + + +@dataclass +class EmbeddingBagConfig: + vocab_size: int = 20000 + embed_dim: int = 64 + max_tokens: int = 128 + dropout: float = 0.1 + num_classes: int = 2 + # allow passthrough of unknown config keys when loading from checkpoints + extra: dict = field(default_factory=dict) + + +def _merge_config(ckpt_cfg: dict | None) -> EmbeddingBagConfig: + """ + Merge checkpoint config with defaults, stashing unknown keys in extra. + + Args: + ckpt_cfg (dict | None): The config dictionary from checkpoint. + + Returns: + EmbeddingBagConfig: The merged configuration object. + """ + ckpt_cfg = ckpt_cfg or {} + cfg_defaults = EmbeddingBagConfig().__dict__ + merged = {**cfg_defaults} + extra = {} + + for k, v in ckpt_cfg.items(): + if k in cfg_defaults: + merged[k] = v + else: + extra[k] = v + + merged["extra"] = extra + + return EmbeddingBagConfig(**merged) + + +def encode_text(text: str, cfg: EmbeddingBagConfig) -> torch.Tensor: + """ + Encode text into token ids using hashing; truncates to cfg.max_tokens. + + Args: + text (str): The input text to encode. + cfg (EmbeddingBagConfig): The configuration with vocab size and max tokens. + + Returns: + torch.Tensor: The tensor of token ids. + """ + tokens = _tokenize(text) + token_ids = [_hash_token(tok, cfg.vocab_size) for tok in tokens[: cfg.max_tokens]] + + if not token_ids: + token_ids = [0] + + return torch.tensor(token_ids, dtype=torch.long) + + +def make_offsets(token_seqs: list[torch.Tensor]) -> tuple[torch.Tensor, torch.Tensor]: + """ + Flatten variable-length token sequences for EmbeddingBag. + + Args: + token_seqs (list[torch.Tensor]): List of 1D tensors of token ids. + + Returns: + tuple[torch.Tensor, torch.Tensor]: (flat_tokens, offsets) where flat_tokens is a 1D tensor of concatenated tokens, + and offsets is a 1D tensor indicating start indices of each sequence. + """ + device = token_seqs[0].device if token_seqs else None + offsets = torch.zeros(len(token_seqs), dtype=torch.long, device=device) + flat = [] + total = 0 + + for i, seq in enumerate(token_seqs): + offsets[i] = total + flat.append(seq) + total += len(seq) + flat_tokens = torch.cat(flat) if flat else torch.tensor([], device=device) + + return flat_tokens, offsets + + +class TinyEmbeddingBagClassifier(nn.Module): + def __init__(self, cfg: EmbeddingBagConfig): + super().__init__() + self.cfg = cfg + self.embedding = nn.EmbeddingBag(cfg.vocab_size, cfg.embed_dim, mode="mean") + self.dropout = nn.Dropout(cfg.dropout) + self.head = nn.Linear(cfg.embed_dim, cfg.num_classes) + + def forward(self, tokens: torch.Tensor, offsets: torch.Tensor) -> torch.Tensor: + """ + Forward pass of the TinyEmbeddingBagClassifier. + + Args: + tokens (torch.Tensor): Tensor of token ids. + offsets (torch.Tensor): Tensor of offsets for EmbeddingBag. + + Returns: + torch.Tensor: The output logits for each class. + """ + x = self.embedding(tokens, offsets) + x = self.dropout(x) + return self.head(x) + + @classmethod + def from_checkpoint( + cls, checkpoint_path: str, device: str = "cpu" + ) -> tuple[TinyEmbeddingBagClassifier, EmbeddingBagConfig]: + """ + Load a TinyEmbeddingBagClassifier from a checkpoint file. + + Raises: + ImportError: If safetensors is required but not installed. + + Returns: + tuple[TinyEmbeddingBagClassifier, EmbeddingBagConfig]: The loaded model and its configuration. + """ + if checkpoint_path.endswith(".safetensors"): + if safe_load_file is None: + raise ImportError( + "safetensors is not installed; cannot load .safetensors checkpoint" + ) + state = safe_load_file(checkpoint_path, device=device) + cfg_path = os.path.splitext(checkpoint_path)[0] + ".json" + cfg_data = {} + if os.path.exists(cfg_path): + with open(cfg_path, "r") as f: + cfg_data = json.load(f) + cfg = _merge_config(cfg_data) + else: + checkpoint = torch.load(checkpoint_path, map_location=device) + cfg = _merge_config(checkpoint.get("config", {})) + state = checkpoint.get("state_dict", checkpoint) + + model = cls(cfg) + model.load_state_dict(state) + model.to(device) + model.eval() + return model, cfg + + def save_checkpoint( + self, + save_path: str, + use_safetensors: bool = True, + ) -> str: + """ + Save model checkpoint. + + Args: + save_path (str): Path to save checkpoint. Extension determines format. + If no extension, .safetensors or .pt will be added based on use_safetensors. + use_safetensors (bool): If True and no extension provided, save as .safetensors. + If False, save as .pt format. Defaults to True. + + Returns: + str: The actual path where the checkpoint was saved. + """ + # Determine save format based on extension or flag + if not save_path.endswith((".safetensors", ".pt")): + ext = ".safetensors" if use_safetensors else ".pt" + save_path = save_path + ext + + os.makedirs(os.path.dirname(save_path) or ".", exist_ok=True) + + if save_path.endswith(".safetensors"): + if safe_save_file is None: + raise ImportError( + "safetensors is not installed; cannot save .safetensors checkpoint. " + "Install with: pip install safetensors" + ) + # Save model weights as safetensors + safe_save_file(self.state_dict(), save_path) + + # Save config as JSON + cfg_path = os.path.splitext(save_path)[0] + ".json" + + with open(cfg_path, "w") as f: + json.dump( + { + "vocab_size": self.cfg.vocab_size, + "embed_dim": self.cfg.embed_dim, + "max_tokens": self.cfg.max_tokens, + "dropout": self.cfg.dropout, + "num_classes": self.cfg.num_classes, + **self.cfg.extra, + }, + f, + indent=2, + ) + else: + # Save as PyTorch checkpoint with config embedded + torch.save( + { + "state_dict": self.state_dict(), + "config": { + "vocab_size": self.cfg.vocab_size, + "embed_dim": self.cfg.embed_dim, + "max_tokens": self.cfg.max_tokens, + "dropout": self.cfg.dropout, + "num_classes": self.cfg.num_classes, + **self.cfg.extra, + }, + }, + save_path, + ) + + return save_path diff --git a/aymurai/models/decision/tokenizer.py b/aymurai/models/decision/tokenizer.py deleted file mode 100644 index bbb21eae..00000000 --- a/aymurai/models/decision/tokenizer.py +++ /dev/null @@ -1,36 +0,0 @@ -import torch -from unidecode import unidecode - -from aymurai.text.tokenizers.spanish import SpanishTokenizer - - -class Tokenizer(object): - def __init__(self, vocab): - self.max_len = 128 - self.tokenizer = SpanishTokenizer() - self.vocab = vocab - - def save(self, path: str): - torch.save(self.vocab, path) - - @classmethod - def load(cls, path: str): - vocab = torch.load(path) - return cls(vocab=vocab) - - def __call__(self, text: str): - text = text.lower() - text = unidecode(text) - return self.tokenizer(text) - - def encode(self, text: int): - tokens = self(text)[: self.max_len] - indices = self.vocab(tokens) - indices = torch.tensor(indices, dtype=torch.int64) - indices = torch.nn.functional.pad(indices, (0, self.max_len - len(indices))) - return indices - - def encode_batch(self, texts): - indices = [self.encode(text) for text in texts] - indices = torch.stack(indices) - return indices diff --git a/aymurai/models/flair/core.py b/aymurai/models/flair/core.py index a3a5ad87..6fae5daf 100644 --- a/aymurai/models/flair/core.py +++ b/aymurai/models/flair/core.py @@ -1,20 +1,21 @@ +import logging import os import re -import logging from copy import deepcopy import flair import numpy as np from flair.data import Sentence -from more_itertools import collapse from flair.models import SequenceTagger +from more_itertools import collapse from aymurai.logger import get_logger -from aymurai.utils.misc import is_url -from aymurai.utils.download import download -from aymurai.meta.types import DataItem, DataBlock -from aymurai.meta.pipeline_interfaces import TrainModule from aymurai.meta.entities import Entity, EntityAttributes +from aymurai.meta.pipeline_interfaces import TrainModule +from aymurai.meta.types import DataBlock, DataItem +from aymurai.settings import settings +from aymurai.utils.download import download +from aymurai.utils.misc import is_url flair.logger.setLevel(logging.ERROR) @@ -46,7 +47,7 @@ def __init__( # load model if is_url(url := basepath): - basepath = os.getenv("AYMURAI_CACHE_BASEPATH", "/resources/cache/aymurai") + basepath = settings.CACHE_BASEPATH model_path = f"{basepath}/{self.__name__}/model.pt" logger.info(f"downloading model on {model_path}") os.makedirs(os.path.dirname(model_path), exist_ok=True) diff --git a/aymurai/models/sentence_encoder/__init__.py b/aymurai/models/sentence_encoder/__init__.py new file mode 100644 index 00000000..c3714e12 --- /dev/null +++ b/aymurai/models/sentence_encoder/__init__.py @@ -0,0 +1,45 @@ +"""Sentence encoder module with sentence-transformers backends. + +Implementations are lazily loaded to avoid importing unavailable dependencies. +Use the factory function `create_encoder()` for automatic backend selection, +or import specific implementations directly from their modules: + + from aymurai.models.sentence_encoder.sentence_transformers_encoder import DistilUSEEncoder +""" + +from aymurai.models.sentence_encoder.base import BaseSentenceEncoder +from aymurai.models.sentence_encoder.factory import ( + EncoderType, + create_encoder, +) + + +def __getattr__(name: str): + """Lazy loading of encoder implementations.""" + if name == "DistilUSEEncoder": + from aymurai.models.sentence_encoder.sentence_transformers_encoder import ( + DistilUSEEncoder, + ) + + return DistilUSEEncoder + + if name == "MultilingualMiniLMEncoder": + from aymurai.models.sentence_encoder.sentence_transformers_encoder import ( + MultilingualMiniLMEncoder, + ) + + return MultilingualMiniLMEncoder + + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +__all__ = [ + # Base interface + "BaseSentenceEncoder", + # Factory + "EncoderType", + "create_encoder", + # Implementations (lazy loaded) + "DistilUSEEncoder", + "MultilingualMiniLMEncoder", +] diff --git a/aymurai/models/sentence_encoder/base.py b/aymurai/models/sentence_encoder/base.py new file mode 100644 index 00000000..daf25c69 --- /dev/null +++ b/aymurai/models/sentence_encoder/base.py @@ -0,0 +1,66 @@ +""" +Abstract base class for sentence encoders. +""" + +import unicodedata +from abc import ABC, abstractmethod +from typing import Iterable, Optional + +import numpy as np + + +class BaseSentenceEncoder(ABC): + """ + Abstract base class defining the interface for sentence encoders. + All encoder implementations must inherit from this class. + """ + + def normalize_text(self, text: str) -> str: + """Normalize text by lowercasing and removing non-alphanumeric characters.""" + text = text.lower() + text = "".join( + char + for char in unicodedata.normalize("NFKD", text) + if char.isalnum() or char.isspace() + ) + return text + + @abstractmethod + def encode( + self, + text_array: list[str], + encoder_type: str, + context_array: Optional[list[str]] = None, + ) -> np.ndarray: + """ + Encode a list of texts into embeddings. + + Args: + text_array: List of texts to encode. + encoder_type: Type of encoder to use ('question_encoder' or 'response_encoder'). + context_array: Optional context for response encoding. + + Returns: + numpy array of embeddings. + """ + pass + + @abstractmethod + def batch_encode( + self, + text_array: Iterable[str], + encoder_type: str, + batch_size: int = 256, + ) -> np.ndarray: + """ + Encode texts in batches. + + Args: + text_array: Iterable of texts to encode. + encoder_type: Type of encoder to use. + batch_size: Number of texts per batch. + + Returns: + numpy array of embeddings. + """ + pass diff --git a/aymurai/models/sentence_encoder/factory.py b/aymurai/models/sentence_encoder/factory.py new file mode 100644 index 00000000..a5d5b541 --- /dev/null +++ b/aymurai/models/sentence_encoder/factory.py @@ -0,0 +1,102 @@ +""" +Factory for creating sentence encoder instances based on configuration. +""" + +import os +from enum import Enum +from typing import Optional + +from aymurai.logger import get_logger +from aymurai.models.sentence_encoder.base import BaseSentenceEncoder + +logger = get_logger(__name__) + + +class EncoderType(str, Enum): + """Available encoder implementations.""" + + DISTILUSE = "distiluse" + MINILM = "minilm" + + +def _get_encoder_type_from_env() -> EncoderType: + """Get encoder type from environment variable.""" + env_value = os.getenv("SENTENCE_ENCODER_TYPE", "distiluse").lower() + try: + return EncoderType(env_value) + except ValueError: + logger.warning( + f"Invalid SENTENCE_ENCODER_TYPE '{env_value}', falling back to 'distiluse'" + ) + return EncoderType.DISTILUSE + + +def _coerce_encoder_type(value: EncoderType | str | None) -> EncoderType: + """ + Normalize encoder type values into EncoderType. + + Args: + value: Encoder type as EncoderType, string, or None. If None, reads from env var. + + Returns: + EncoderType: Normalized encoder type. + """ + if value is None: + return _get_encoder_type_from_env() + + if isinstance(value, EncoderType): + return value + + if isinstance(value, str): + try: + return EncoderType(value.lower()) + except ValueError as exc: + raise ValueError(f"Unknown encoder type: {value}") from exc + + raise TypeError(f"Unsupported encoder type value: {type(value)!r}") + + +def create_encoder( + encoder_type: EncoderType | str | None = None, + device: Optional[str] = None, +) -> BaseSentenceEncoder: + """ + Factory function to create the appropriate sentence encoder. + + Args: + encoder_type: Type of encoder to create. Accepts EncoderType or string. + If None, reads from SENTENCE_ENCODER_TYPE env var + (defaults to 'distiluse'). + device: Device for sentence-transformers models. + + Returns: + An instance of BaseSentenceEncoder. + + Environment Variables: + SENTENCE_ENCODER_TYPE: One of 'distiluse', 'minilm'. + - 'distiluse': Use distiluse-base-multilingual-cased-v2 + - 'minilm': Use paraphrase-multilingual-MiniLM-L12-v2 + + Raises: + ImportError: If required dependencies are not available. + ValueError: If an invalid encoder type is specified. + """ + encoder_type = _coerce_encoder_type(encoder_type) + + # Create the appropriate encoder + if encoder_type == EncoderType.DISTILUSE: + from aymurai.models.sentence_encoder.sentence_transformers_encoder import ( + DistilUSEEncoder, + ) + + return DistilUSEEncoder(device=device) + + elif encoder_type == EncoderType.MINILM: + from aymurai.models.sentence_encoder.sentence_transformers_encoder import ( + MultilingualMiniLMEncoder, + ) + + return MultilingualMiniLMEncoder(device=device) + + else: + raise ValueError(f"Unknown encoder type: {encoder_type}") diff --git a/aymurai/models/sentence_encoder/sentence_transformers_encoder.py b/aymurai/models/sentence_encoder/sentence_transformers_encoder.py new file mode 100644 index 00000000..53668f01 --- /dev/null +++ b/aymurai/models/sentence_encoder/sentence_transformers_encoder.py @@ -0,0 +1,124 @@ +""" +Sentence Transformers encoder implementations. +Compatible with Apple Silicon (M1/M2/M3) via PyTorch MPS backend. +""" + +from typing import Iterable, Optional + +import numpy as np +from sentence_transformers import SentenceTransformer + +from aymurai.logger import get_logger +from aymurai.models.sentence_encoder.base import BaseSentenceEncoder + +logger = get_logger(__name__) + + +class DistilUSEEncoder(BaseSentenceEncoder): + """ + Knowledge-distilled Universal Sentence Encoder using sentence-transformers. + + This is a PyTorch-based distillation of the original USE multilingual model. + Supports 50+ languages and produces 512-dimensional embeddings. + + Compatible with Apple Silicon via MPS backend. + """ + + MODEL_NAME = "sentence-transformers/distiluse-base-multilingual-cased-v2" + + def __init__(self, device: Optional[str] = None): + """ + Initialize the DistilUSE encoder. + + Args: + device: Device to run the model on. If None, auto-detects + (will use 'mps' on Apple Silicon, 'cuda' if available, else 'cpu'). + """ + self.model = SentenceTransformer(self.MODEL_NAME, device=device) + logger.info(f"Loaded {self.MODEL_NAME} on device: {self.model.device}") + + def encode( + self, + text_array: list[str], + encoder_type: str, + context_array: Optional[list[str]] = None, + ) -> np.ndarray: + """ + Encode texts into embeddings. + + Note: encoder_type and context_array are kept for API compatibility + but are not used since sentence-transformers uses a single encoder. + """ + input_array = [self.normalize_text(text) for text in text_array] + return self.model.encode(input_array, convert_to_numpy=True) + + def batch_encode( + self, + text_array: Iterable[str], + encoder_type: str, + batch_size: int = 256, + ) -> np.ndarray: + """Encode texts in batches with progress bar.""" + text_list = list(text_array) + input_array = [self.normalize_text(text) for text in text_list] + return self.model.encode( + input_array, + batch_size=batch_size, + show_progress_bar=True, + convert_to_numpy=True, + ) + + +class MultilingualMiniLMEncoder(BaseSentenceEncoder): + """ + Multilingual MiniLM encoder using sentence-transformers. + + Higher quality embeddings than DistilUSE with faster inference. + Supports 50+ languages and produces 384-dimensional embeddings. + + Compatible with Apple Silicon via MPS backend. + """ + + MODEL_NAME = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" + + def __init__(self, device: Optional[str] = None): + """ + Initialize the Multilingual MiniLM encoder. + + Args: + device: Device to run the model on. If None, auto-detects + (will use 'mps' on Apple Silicon, 'cuda' if available, else 'cpu'). + """ + self.model = SentenceTransformer(self.MODEL_NAME, device=device) + logger.info(f"Loaded {self.MODEL_NAME} on device: {self.model.device}") + + def encode( + self, + text_array: list[str], + encoder_type: str, + context_array: Optional[list[str]] = None, + ) -> np.ndarray: + """ + Encode texts into embeddings. + + Note: encoder_type and context_array are kept for API compatibility + but are not used since sentence-transformers uses a single encoder. + """ + input_array = [self.normalize_text(text) for text in text_array] + return self.model.encode(input_array, convert_to_numpy=True) + + def batch_encode( + self, + text_array: Iterable[str], + encoder_type: str, + batch_size: int = 256, + ) -> np.ndarray: + """Encode texts in batches with progress bar.""" + text_list = list(text_array) + input_array = [self.normalize_text(text) for text in text_list] + return self.model.encode( + input_array, + batch_size=batch_size, + show_progress_bar=True, + convert_to_numpy=True, + ) diff --git a/aymurai/models/usem/core.py b/aymurai/models/usem/core.py deleted file mode 100644 index 4fbd6746..00000000 --- a/aymurai/models/usem/core.py +++ /dev/null @@ -1,109 +0,0 @@ -import unicodedata -from multiprocessing import cpu_count -from typing import Iterable, Optional - -import numpy as np -import tensorflow as tf -import tensorflow_hub as hub -import tensorflow_text # noqa -from more_itertools import chunked -from tqdm.auto import tqdm - -from aymurai.logger import get_logger - -logger = get_logger(__name__) - -N_JOBS = cpu_count() - - -class USEMQA: - """ - Base class for USEM (Universal Sentence Encoder Multilingual QA) - """ - - def __init__( - self, - usem_qa_url: str = "https://tfhub.dev/google/universal-sentence-encoder-multilingual-qa/3", - ): - self.embed = hub.load(usem_qa_url) - - def normalize_text(self, text: str) -> str: - text = text.lower() - text = "".join( - char - for char in unicodedata.normalize("NFKD", text) - if char.isalnum() or char.isspace() - ) - return text - - def encode( - self, - text_array: list[str], - encoder_type: str, - context_array: Optional[list[str]] = None, - ) -> np.ndarray: - input_array = [self.normalize_text(text) for text in text_array] - - if encoder_type == "response_encoder": - encoder_params = { - "input": tf.constant(input_array), - "context": tf.constant(context_array), - } - - if encoder_type == "question_encoder": - encoder_params = { - "input": tf.constant(input_array), - } - - encoded = self.embed.signatures[encoder_type](**encoder_params) - encoded = encoded["outputs"] - - return encoded - - def batch_encode( - self, - text_array: Iterable[str], - encoder_type: str, - batch_size: int = 256, - ) -> np.ndarray: - text_chunks = chunked(text_array, batch_size) - - encoded = [ - self.encode(text_chunk, encoder_type) - for text_chunk in tqdm( - text_chunks, - desc="creating USEM vectors...", - total=len(text_array) // batch_size, - ) - ] - - encoded = np.vstack(encoded) - - return encoded - - -class SentenceRetrieval: - def __init__( - self, - categories: list[str], - response_embeddings_path: str, - ): - self.usem = USEMQA() - self.categories = categories - self.usem_vectors = self.load_usem_vectors(response_embeddings_path) - - def load_usem_vectors(self, file_path): - usem_vectors = np.load(file_path) - return usem_vectors - - def retrieve(self, text: str, top_k: int = 10) -> list[str]: - query_vector = self.usem.encode( - [text], - encoder_type="question_encoder", - ) - - products = np.inner(query_vector, self.usem_vectors)[0] - similar_idx = np.flip(products.argsort())[:top_k] - similar_sentences = [self.categories[idx] for idx in similar_idx] - - return similar_sentences diff --git a/aymurai/scripts/pipelines_download.py b/aymurai/scripts/pipelines_download.py new file mode 100644 index 00000000..f797b613 --- /dev/null +++ b/aymurai/scripts/pipelines_download.py @@ -0,0 +1,19 @@ +import os + +from aymurai.logger import get_logger +from aymurai.pipeline.pipeline import AymurAIPipeline +from aymurai.settings import settings + +logger = get_logger(__name__) + +RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH + + +def pipelines_download(): + logger.info("Loading pipelines and exit.") + AymurAIPipeline.load( + os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "flair-anonymizer") + ) + AymurAIPipeline.load( + os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "datapublic") + ) diff --git a/aymurai/settings.py b/aymurai/settings.py index 9fdd5331..259d03ed 100644 --- a/aymurai/settings.py +++ b/aymurai/settings.py @@ -1,15 +1,25 @@ +import json import os +from copy import deepcopy from pathlib import Path from dotenv import load_dotenv +from pydantic import AliasChoices, ConfigDict, Field, FilePath, field_validator from pydantic_settings import BaseSettings -from pydantic import FilePath, ConfigDict, field_validator import aymurai +from aymurai.utils.yaml_data import load_yaml PARENT = Path(aymurai.__file__).parent +DEFAULT_DISAMBIGUATION_LABEL_POLICIES = load_yaml( + str(PARENT / "config" / "default_disambiguation_label_policies.yml") +) + +DEFAULT_RENDER_POLICY = {"suffix_mode": "auto", "suffix_threshold": 1} + + def load_env(): load_dotenv(".env") @@ -22,7 +32,7 @@ def load_env(): class Settings(BaseSettings): - model_config = ConfigDict(case_sensitive=True) + model_config = ConfigDict(case_sensitive=False) CORS_ORIGINS: list[str] | str = ",".join( [ @@ -49,6 +59,10 @@ def assemble_cors_origins(cls, v) -> list[str]: SQLALCHEMY_DATABASE_URI: str = "sqlite:////resources/cache/sqlite/database.db" RESOURCES_BASEPATH: str = "/resources" + CACHE_BASEPATH: str = Field( + default="/resources/cache", + validation_alias=AliasChoices("AYMURAI_CACHE_BASEPATH", "CACHE_BASEPATH"), + ) # Alembic Config for running migrations ALEMBIC_INI_PATH: FilePath = PARENT / "alembic.ini" @@ -58,8 +72,42 @@ def assemble_cors_origins(cls, v) -> list[str]: # Cachetools settings MEMORY_CACHE_MAXSIZE: int = 1 MEMORY_CACHE_TTL: int = 60 + TORCH_NUM_THREADS: int = 4 LIBREOFFICE_BIN: str = "libreoffice" + PDF_WATERMARK_FONT_REGULAR: str | None = None + PDF_WATERMARK_FONT_BOLD: str | None = None + ANONYMIZATION_METADATA_CREATOR: str = "AymurAI" + ANONYMIZATION_METADATA_PRODUCER: str = "AymurAI" + + # Disambiguation Config + + # Fuzzy Matching + THRESHOLD: int = 70 + + # Label policies (JSON dict: label -> {disambiguation, anonymize}) + DISAMBIGUATION_LABEL_POLICIES: dict | None = Field( + default_factory=lambda: deepcopy(DEFAULT_DISAMBIGUATION_LABEL_POLICIES) + ) + + # Render policy (JSON dict) + RENDER_POLICY: dict | None = Field( + default_factory=lambda: deepcopy(DEFAULT_RENDER_POLICY) + ) + + @field_validator("DISAMBIGUATION_LABEL_POLICIES", "RENDER_POLICY", mode="before") + @classmethod + def parse_policies(cls, v): + if v is None or v == "": + return None + if isinstance(v, str): + return json.loads(v) + return v + + FRONTEND_DIST_DIR: str = Field( + default="frontend-dist", + validation_alias=AliasChoices("AYMURAI_FRONTEND_DIST_DIR", "FRONTEND_DIST_DIR"), + ) load_env() diff --git a/aymurai/text/anonymization.py b/aymurai/text/anonymization.py deleted file mode 100644 index fe6fb14f..00000000 --- a/aymurai/text/anonymization.py +++ /dev/null @@ -1,766 +0,0 @@ -import os -import re -import tempfile -import xml.sax.saxutils -import zipfile -from copy import deepcopy -from glob import glob -from unicodedata import normalize - -import numpy as np -import pandas as pd -from docx import Document -from docx.enum.text import WD_ALIGN_PARAGRAPH -from docx.opc.constants import RELATIONSHIP_TYPE -from docx.oxml.shared import OxmlElement, qn -from docx.shared import Inches, Pt, RGBColor -from jiwer import cer -from joblib import hash -from lxml import etree -from more_itertools import flatten - -from aymurai.logger import get_logger -from aymurai.meta.pipeline_interfaces import Transform -from aymurai.models.flair.utils import FlairTextNormalize -from aymurai.utils.alignment.core import align_text, tokenize -from aymurai.utils.cache import cache_load, cache_save, get_cache_key - -logger = get_logger(__file__) - - -REGEX_PARAGRAPH = r"((?.*?)(\/w:p\b)" -REGEX_FRAGMENT = r"(?(?P.*?)(<.*?\/w:t)" - - -class DocAnonymizer(Transform): - """ - Anonymize document by replacing sensitive data with label tokens - """ - - def __init__(self, use_cache: bool = False, **kwargs): - self.use_cache = use_cache - self.kwargs = kwargs - - def unzip_document(self, doc_path: str, output_dir: str) -> None: - """ - Unzips the document file to the specified output directory. - - Args: - doc_path (str): The path to the document file. - output_dir (str): The directory where the contents of the document - file will be extracted. - """ - # Ensure the output directory exists - os.makedirs(output_dir, exist_ok=True) - - # Open the doc file as a zip file - with zipfile.ZipFile(doc_path, "r") as doc_zip: - # Extract all the contents to the output directory - doc_zip.extractall(output_dir) - logger.info(f"unzipped {doc_path} to {output_dir}") - - def index_paragraphs(self, file: str) -> list[dict]: - """ - Indexes the paragraphs of an XML file. - - Args: - file (str): The path to the XML file to be indexed. - - Returns: - list[dict]: A list of dictionaries representing the indexed paragraphs. - """ - # Read the XML file - with open(file) as f: - xml = f.read() - - paragraphs = [] - paragraph_index = 0 - - # Find all paragraphs in the XML file - for match in re.finditer(REGEX_PARAGRAPH, xml): - paragraph = match.group("paragraph") - paragraph_start = match.start("paragraph") - paragraph_end = match.end("paragraph") - fragments = [] - fragment_index = 0 - - # Find all text fragments in the paragraph - for fragment in re.finditer(REGEX_FRAGMENT, paragraph): - text = fragment.group("text") - start = fragment.start("text") - end = fragment.end("text") - - fragment_dict = { - "text": text, - "normalized_text": FlairTextNormalize.normalize_text(text), - "start": start, - "end": end, - "fragment_index": fragment_index, - "paragraph_index": paragraph_index, - } - fragments.append(fragment_dict) - fragment_index += 1 - - # Join all fragments as plain text - plain_text = "".join( - [fragment["normalized_text"] for fragment in fragments] - ) - - paragraphs.append( - { - "plain_text": plain_text, - "metadata": { - "start": paragraph_start, - "end": paragraph_end, - "fragments": fragments, - "xml_file": os.path.basename(file), - }, - } - ) - paragraph_index += 1 - - return paragraphs - - def match_paragraphs_with_predictions( - self, - paragraphs: list[dict], - predictions: list[dict], - ) -> list[dict]: - """ - Matches each paragraph with its corresponding predictions. - - Args: - paragraphs (list[dict]): A list of dictionaries representing the paragraphs. - predictions (list[dict]): A list of dictionaries representing - the predictions. - - Returns: - list[dict]: A list of dictionaries representing - the matched paragraphs with predictions. - """ - - paragraphs = deepcopy(paragraphs) - - # Hash prediction documents - pred_hashes = [hash(prediction["document"]) for prediction in predictions] - idx2hash = {i: _hash for i, _hash in enumerate(pred_hashes)} - - # Hash paragraphs - paragraphs = [ - paragraph - | {"hash": hash(normalize("NFKC", paragraph["plain_text"].strip()))} - for paragraph in paragraphs - ] - - # Assign prediction indices to each paragraph by hash - hash2idx = { - paragraph["hash"]: np.where(np.array(pred_hashes) == paragraph["hash"])[ - 0 - ].tolist() - for paragraph in paragraphs - } - - paragraphs = [ - paragraph | {"pred_indices": hash2idx[paragraph["hash"]]} - for paragraph in paragraphs - ] - - # Identify missing indices - missing_indices = list( - set(idx2hash.keys()) - set(flatten(list(hash2idx.values()))) - ) - - if missing_indices: - # Assign prediction indices to each paragraph by lowest CER - target_texts = np.array( - [prediction["document"] for prediction in predictions] - )[missing_indices] - - missing_paragraphs = [ - paragraph for paragraph in paragraphs if not paragraph["pred_indices"] - ] - - for missing_paragraph in missing_paragraphs: - source_text = missing_paragraph["plain_text"] - min_cer_idx = np.argmin( - [cer(source_text, target_text) for target_text in target_texts] - ) - missing_paragraph["pred_indices"] = [missing_indices[min_cer_idx]] - - # Assign document text and labels - paragraphs = [ - paragraph - | { - "document": predictions[paragraph["pred_indices"][0]]["document"], - "labels": predictions[paragraph["pred_indices"][0]]["labels"], - } - for paragraph in paragraphs - ] - - return paragraphs - - def unify_consecutive_labels( - self, sample: dict, text_key: str = "document" - ) -> list[dict]: - """ - Unifies consecutive labels in a sample. - - Args: - sample (dict): A dictionary representing the sample. - text_key (str, optional): The key for the text in the sample dictionary. - Defaults to "document". - - Returns: - list[dict]: A list of dictionaries representing the unified labels. - """ - sample = deepcopy(sample) - - # Extract labels and document text - labels = sample["labels"] - document = sample[text_key] - - # Reorder labels based on start indices - labels = sorted(labels, key=lambda x: x["start_char"]) - - unified_labels = [] - current_group = None - - # Iterate over labels - for label in labels: - # Get attributes - text = label["attrs"]["aymurai_alt_text"] or label["text"] - start_char = label["attrs"]["aymurai_alt_start_char"] or label["start_char"] - end_char = label["attrs"]["aymurai_alt_end_char"] or label["end_char"] - aymurai_label = label["attrs"]["aymurai_label"] - - if current_group is None: - # Start a new group with the current label - current_group = { - "text": text, - "start_char": start_char, - "end_char": end_char, - "aymurai_label": aymurai_label, - } - elif ( - current_group["aymurai_label"] == aymurai_label - and (start_char - current_group["end_char"]) <= 1 - ): - # Extend the current group with the current label - current_group["end_char"] = end_char - else: - # Finish the current group and start a new one - current_group["text"] = document[ - current_group["start_char"] : current_group["end_char"] + 1 - ] - unified_labels.append(current_group) - current_group = { - "text": text, - "start_char": start_char, - "end_char": end_char, - "aymurai_label": aymurai_label, - } - - # Finish the last group - if current_group is not None: - current_group["text"] = document[ - current_group["start_char"] : current_group["end_char"] + 1 - ] - unified_labels.append(current_group) - - return unified_labels - - def replace_labels_in_text(self, pred: dict, text_key: str = "document") -> str: - """ - Replaces labels in the text with anonymized tokens. - - Args: - pred (dict): A dictionary representing the prediction. - text_key (str, optional): The key for the text in the prediction dictionary. - Defaults to "document". - - Returns: - str: The text with replaced labels. - """ - pred = deepcopy(pred) - doc = pred[text_key] - - # Unify consecutive labels - unified_labels = self.unify_consecutive_labels(pred, text_key) - - # Initialize the offset - offset = 0 - - # Replace labels in the text - for unified_label in unified_labels: - # Adjust start and end character indices of the label - start_char = unified_label["start_char"] + offset - end_char = unified_label["end_char"] + offset - len_text_to_replace = end_char - start_char - - # Replace the text with the anonymized token - aymurai_label = f" <{unified_label['aymurai_label']}>" - len_aymurai_label = len(aymurai_label) - - doc = doc[:start_char] + aymurai_label + doc[end_char:] - - # Update the offset - offset += len_aymurai_label - len_text_to_replace - - return re.sub(r" +", " ", doc).strip() - - def erase_duplicates_justseen(self, series: pd.Series) -> pd.Series: - """ - Erases duplicates that were just seen between two consecutive rows. - - Args: - series (pd.Series): The pandas Series to be processed. - - Returns: - pd.Series: The processed pandas Series. - """ - return pd.Series( - [ - ( - "" - if (i > 0 and series.iloc[i] == series.iloc[i - 1]) - else series.iloc[i] - ) - for i in range(len(series)) - ] - ) - - def parse_token_indices(self, sample: dict) -> pd.DataFrame: - """ - Parses the token indices from a sample. - - Args: - sample (dict): A dictionary representing the sample. - - Returns: - pd.DataFrame: A pandas DataFrame representing the parsed token indices. - """ - original_text = " ".join( - [fragment["text"] for fragment in sample["metadata"]["fragments"]] - ) - anonymized_text = self.replace_labels_in_text(sample) - - aligned = align_text( - " " + original_text + " ", - " " + anonymized_text + " ", - ) - aligned["target"] = self.erase_duplicates_justseen(aligned["target"]) - - xml_file = sample["metadata"]["xml_file"] - - tokens = [] - for i, fragment in enumerate(sample["metadata"]["fragments"]): - text = fragment["text"] - tokenized_text = tokenize(text) - paragraph_index = fragment["paragraph_index"] - - # Use re.finditer to locate each instance of tokens in the text - token_matches = list( - re.finditer(r"\S+", text) - ) # \S+ matches any non-whitespace sequence - - # Loop over tokenized text and token_matches in parallel - for j, (token, match) in enumerate(zip(tokenized_text, token_matches)): - start = sample["metadata"]["start"] + fragment["start"] + match.start() - end = start + len(token) - - tokens.append((xml_file, paragraph_index, i, j, token, start, end)) - - tokens = pd.DataFrame( - tokens, - columns=[ - "xml_file", - "paragraph_index", - "fragment_index", - "token_index", - "token", - "start_char", - "end_char", - ], - ) - - tokens = pd.concat( - [tokens, aligned["target"].iloc[1:-1].reset_index(drop=True)], axis=1 - ) - - tokens["target"] = tokens["target"].fillna("") - - return tokens - - def normalize_document(self, xml_content: str) -> str: - """ - Normalizes the XML document by removing extra spaces, preserving line breaks, - and removing hyperlinks while preserving text content. - - Args: - xml_content (str): The XML content to be normalized. - - Returns: - str: The normalized XML content. - """ - # Parse the XML content - parser = etree.XMLParser(ns_clean=True) - root = etree.fromstring(xml_content.encode("utf-8"), parser) - - # Extract namespaces - namespaces = {k: v for k, v in root.nsmap.items() if k} - - # Remove hyperlinks but preserve the text content - for hyperlink in root.xpath("//w:hyperlink", namespaces=namespaces): - parent = hyperlink.getparent() - index = list(parent).index(hyperlink) - - # Move all text-containing children (e.g., w:r elements) outside the hyperlink tag - for child in hyperlink: - parent.insert(index, child) - index += 1 - parent.remove(hyperlink) # Remove the element itself - - # Process each paragraph - for wp in root.xpath("//w:p", namespaces=namespaces): - first = True - - # Find all w:r elements containing w:t elements - for wr in wp.xpath(".//w:r", namespaces=namespaces): - wt = wr.find(".//w:t", namespaces) - if wt is not None and wt.text: - # Normalize spaces within the text, preserving new line breaks - wt.text = re.sub(r"[^\S\r\n]+", " ", wt.text) - - # Add a leading space if not the first fragment in the paragraph - if not first: - wt.text = " " + wt.text.lstrip() - else: - wt.text = wt.text.lstrip() - first = False - - # Remove trailing spaces from all fragments - wt.text = wt.text.rstrip() - - # Set the xml:space attribute to preserve - wt.set( - "{http://www.w3.org/XML/1998/namespace}space", - "preserve", - ) - - # Check if the text is empty after normalization - if not wt.text or wt.text.strip() == "": - # Remove the w:r element from its parent - wr.getparent().remove(wr) - - # Write back the XML content to a string - xml_str = etree.tostring(root, encoding="unicode", pretty_print=True) - - return xml_str - - def replace_text_in_xml(self, paragraphs: list[dict], base_dir: str) -> None: - """ - Replaces text in XML files based on the provided paragraphs - and saves the modified files. - - Args: - paragraphs (list[dict]): A list of dictionaries representing - the paragraphs to be replaced. - base_dir (str): The base directory where the XML files are located. - """ - tokens = pd.concat( - [self.parse_token_indices(sample) for sample in paragraphs], - ignore_index=True, - ) - - fragments = ( - tokens.groupby(["xml_file", "paragraph_index", "fragment_index"]) - .agg({"target": " ".join, "start_char": "min", "end_char": "max"}) - .reset_index() - ) - - for xml_file, group in fragments.groupby("xml_file"): - group = group.sort_values("end_char", ascending=False) - - with open(f"{base_dir}/word/{xml_file}", "r+") as file: - content = file.read() - - for _, r in group.iterrows(): - start_char = r["start_char"] - end_char = r["end_char"] - - target = r["target"] - target = re.sub(r"[^\S\r\n]+", " ", target) - - # Escape XML special characters - target = xml.sax.saxutils.escape(target) - - content = content[:start_char] + target + content[end_char:] - - # MUST be at the end to dont screw up the indexes - content = self.normalize_document(content) - - file.seek(0) - file.write(content) - file.truncate() - - def add_files_to_zip(self, zip_file: zipfile.ZipFile, directory: str) -> None: - """ - Adds all files in the specified directory to a zip file. - - Args: - zip_file (zipfile.ZipFile): The zip file to add the files to. - directory (str): The directory containing the files to be added. - """ - for root, _, files in os.walk(directory): - for file in files: - file_path = os.path.join(root, file) - zip_file.write(file_path, os.path.relpath(file_path, directory)) - - def add_hyperlink( - self, - paragraph, - text, - url, - font_name="Archivo", - size=10, - color=RGBColor(115, 190, 250), - italic=False, - bold=True, - underline=True, - ) -> None: - """ - Adds a formatted hyperlink to a given paragraph in a Word document. - - Notes: - - This method directly manipulates the underlying XML of the paragraph to insert a hyperlink, - as python-docx does not natively support hyperlinks. - - The hyperlink will be appended to the end of the given paragraph. - - Formatting options (font, size, color, italic, bold, underline) are applied to the hyperlink text. - - Args: - paragraph: The python-docx paragraph object to which the hyperlink will be added. - text (str): The display text for the hyperlink. - url (str): The URL that the hyperlink points to. - font_name (str, optional): The font name to use for the hyperlink text. Defaults to "Archivo". - size (int, optional): The font size (in points) for the hyperlink text. Defaults to 10. - color (RGBColor, optional): The font color as an RGBColor tuple. Defaults to RGBColor(115, 190, 250). - italic (bool, optional): Whether the hyperlink text should be italicized. Defaults to False. - bold (bool, optional): Whether the hyperlink text should be bold. Defaults to True. - underline (bool, optional): Whether the hyperlink text should be underlined. Defaults to True. - - Raises: - ValueError: If the paragraph is not a valid python-docx paragraph object. - """ # noqa: E501 - # Create the hyperlink relationship - part = paragraph.part - r_id = part.relate_to(url, RELATIONSHIP_TYPE.HYPERLINK, is_external=True) - - # Create the hyperlink element - hyperlink = OxmlElement("w:hyperlink") - hyperlink.set(qn("r:id"), r_id) - - # Create a new run - new_run = OxmlElement("w:r") - - # Set run properties (formatting) - rPr = OxmlElement("w:rPr") - - # Set font - if font_name: - font = OxmlElement("w:rFonts") - font.set(qn("w:ascii"), font_name) - font.set(qn("w:hAnsi"), font_name) - rPr.append(font) - - # Set color - if color: - c = OxmlElement("w:color") - c.set(qn("w:val"), f"{color[0]:02x}{color[1]:02x}{color[2]:02x}") - rPr.append(c) - - # Set italic - if italic: - i = OxmlElement("w:i") - rPr.append(i) - - # Set bold - if bold: - b = OxmlElement("w:b") - rPr.append(b) - - # Set underline - added for links - if underline: - u = OxmlElement("w:u") - u.set(qn("w:val"), "single") # single underline - rPr.append(u) - - # Set size - sz = OxmlElement("w:sz") - sz.set(qn("w:val"), str(size * 2)) # Word uses half-points - rPr.append(sz) - - # Add properties to run - new_run.append(rPr) - - # Set text - t = OxmlElement("w:t") - t.text = text - new_run.append(t) - - # Add run to hyperlink - hyperlink.append(new_run) - - # Add hyperlink to paragraph - paragraph._p.append(hyperlink) - - def _add_watermark_to_footer( - self, - footer, - alignment, - font_name="Archivo", - hyperlink_text="AymurAI", - hyperlink_url="https://www.aymurai.info/", - watermark_text="Documento anonimizado por AymurAI", - ) -> None: - """ - Adds a watermark text to the footer of a document. - - Args: - footer: The footer object to which the watermark will be added. - alignment: The alignment setting for the paragraph (e.g., left, center, right). - font_name (str, optional): The font name to use for the watermark text. Defaults to "Archivo". - hyperlink_text (str, optional): The text to be hyperlinked. Defaults to "AymurAI". - hyperlink_url (str, optional): The URL to link "AymurAI" to. Defaults to "https://www.aymurai.info/". - watermark_text (str): The text to be used as the watermark. Defaults to "Documento anonimizado por AymurAI". - """ # noqa: E501 - paragraph = footer.add_paragraph() - paragraph.alignment = alignment - - if hyperlink_url and hyperlink_text in watermark_text: - parts = watermark_text.split(hyperlink_text, 1) - before_text = parts[0] - after_text = parts[1] if len(parts) > 1 else "" - - # Add text before the hyperlink - if before_text: - run = paragraph.add_run(before_text) - run.font.name = "Archivo" - run.font.color.rgb = RGBColor(192, 192, 192) - run.font.size = Pt(10) - - # Add hyperlink - self.add_hyperlink(paragraph, hyperlink_text, hyperlink_url) - - # Add text after the hyperlink - if after_text: - run = paragraph.add_run(after_text) - run.font.name = font_name - run.font.color.rgb = RGBColor(192, 192, 192) - run.font.size = Pt(10) - - else: - # Just add the full text without a hyperlink - run = paragraph.add_run(watermark_text) - run.font.name = font_name - run.font.color.rgb = RGBColor(192, 192, 192) - run.font.size = Pt(10) - - def add_footer_watermark(self, doc_path) -> None: - """ - Adds a watermark to the footer of each section in a Word document. - - Args: - doc_path: Path to the document - """ - document = Document(doc_path) - processed_footers = set() - - for section in document.sections: - section.footer_distance = Inches(0.1) - - # List of (footer_obj, alignment) tuples - footers = [(section.footer, WD_ALIGN_PARAGRAPH.RIGHT)] # Odd/default - if section.even_page_footer is not None: - footers.append((section.even_page_footer, WD_ALIGN_PARAGRAPH.LEFT)) - if section.different_first_page_header_footer: - footers.append((section.first_page_footer, WD_ALIGN_PARAGRAPH.RIGHT)) - - for footer, alignment in footers: - if id(footer) not in processed_footers: - self._add_watermark_to_footer(footer, alignment) - processed_footers.add(id(footer)) - - document.save(doc_path) - - def create_docx(self, xml_directory, output_file) -> None: - """ - Creates a new DOCX file by adding XML components from the specified directory. - - Args: - xml_directory (str): The directory containing the XML components. - output_file (str): The path to the output DOCX file. - """ - # Create a new zip file - with zipfile.ZipFile(output_file, "w") as docx: - # Add XML components - self.add_files_to_zip(docx, xml_directory) - - def __call__(self, item: dict, preds: list[dict], output_dir: str = ".") -> None: - """ - Performs the anonymization process on a document. - - Args: - item (dict): The document item to be anonymized. - preds (list[dict]): The list of predictions for the document. - output_dir (str, optional): The directory to save the anonymized document. - Defaults to ".". - - Raises: - ValueError: If the document has an extension other than `.docx`. - """ - item_path = item["path"] - - if not os.path.splitext(item_path)[-1] == ".docx": - raise ValueError("Only `.docx` extension is allowed.") - - if not item.get("data"): - item["data"] = {} - - cache_key = get_cache_key(item_path, self.__name__) - if self.use_cache and (cache_data := cache_load(key=cache_key)): - paragraphs = cache_data - else: - # Unzip document into a temporary directory - with tempfile.TemporaryDirectory() as tempdir: - self.unzip_document(item_path, tempdir) - - # Parse XML files - xml_files = glob(f"{tempdir}/**/*.xml", recursive=True) - paragraphs = (self.index_paragraphs(file) for file in xml_files) - paragraphs = list(flatten(paragraphs)) - - # Filter out empty paragraphs - paragraphs = [ - paragraph - for paragraph in paragraphs - if paragraph["plain_text"].strip() - ] - - # Matching - paragraphs = self.match_paragraphs_with_predictions(paragraphs, preds) - - # Edit XML filess - self.replace_text_in_xml(paragraphs, tempdir) - - # Recreate anonymized document - os.makedirs(output_dir, exist_ok=True) - self.create_docx( - tempdir, - f"{output_dir}/{os.path.basename(item_path)}", - ) - - # Add watermark to the footer - self.add_footer_watermark(f"{output_dir}/{os.path.basename(item_path)}") - - if self.use_cache: - cache_save(paragraphs, key=cache_key) diff --git a/aymurai/text/anonymization/__init__.py b/aymurai/text/anonymization/__init__.py new file mode 100644 index 00000000..51f3a65b --- /dev/null +++ b/aymurai/text/anonymization/__init__.py @@ -0,0 +1,21 @@ +from aymurai.text.anonymization.alignment import replace_labels_in_text +from aymurai.text.anonymization.base import ( + BaseAnonymizer, + InvalidDocumentAnonymizer, + get_anonymizer, + register_anonymizer, + supported_extensions, +) +from aymurai.text.anonymization.docx import DocxAnonymizer +from aymurai.text.anonymization.pdf import PdfAnonymizer + +__all__ = [ + "BaseAnonymizer", + "DocxAnonymizer", + "PdfAnonymizer", + "InvalidDocumentAnonymizer", + "get_anonymizer", + "register_anonymizer", + "supported_extensions", + "replace_labels_in_text", +] diff --git a/aymurai/text/anonymization/alignment.py b/aymurai/text/anonymization/alignment.py new file mode 100644 index 00000000..e4f2547e --- /dev/null +++ b/aymurai/text/anonymization/alignment.py @@ -0,0 +1,463 @@ +import os +import re +from copy import deepcopy +from unicodedata import normalize + +import numpy as np +import pandas as pd +from jiwer import cer +from joblib import hash +from more_itertools import flatten + +from aymurai.meta.api_interfaces import LabelPolicy +from aymurai.models.flair.utils import FlairTextNormalize +from aymurai.utils.alignment.core import align_text, tokenize + +REGEX_PARAGRAPH = r"((?.*?)(\/w:p\b)" +REGEX_FRAGMENT = r"(?(?P.*?)(<.*?\/w:t)" + + +def resolve_render_token(label: dict, render_context: dict | None = None) -> str: + """ + Resolves the render token for a label using the current render context. + + Args: + label (dict): Label dictionary with attrs. + + Returns: + str: Render token to insert in the document. + """ + if not render_context: + return label["attrs"]["aymurai_label"] + + render_policy = render_context["render_policy"] + label_policies = render_context["label_policies"] + count_by_base = render_context["count_by_base"] + index_by_entity = render_context["index_by_entity"] + + attrs = label.get("attrs") or {} + base = attrs.get("aymurai_label") + + label_policy = label_policies.get(base.upper(), LabelPolicy()) + + subclasses = attrs.get("aymurai_label_subclass") or [] + if label_policy.use_subclass_when_available and subclasses: + base = subclasses[0].upper() + + if not base: + base = label.get("label") or label.get("text") or "ENT" + + entity_id = attrs.get("canonical_entity_id") or label.get("text") + key = (base, str(entity_id)) + index = index_by_entity.get(key) + + if render_policy.suffix_mode == "never" or index is None: + return base + + if render_policy.suffix_mode == "auto": + if count_by_base.get(base, 0) <= render_policy.suffix_threshold: + return base + + return f"{base}_{index}" + + +def _label_replacement_start(label: dict) -> int: + """ + Determines the start character index for a label, considering possible alternative attributes. + + Args: + label (dict): Label dictionary which may contain alternative start character attributes. + + Returns: + int: The start character index for the label. + """ + attrs = label.get("attrs") or {} + alt_start = attrs.get("aymurai_alt_start_char") + start_char = label.get("start_char") + return int(alt_start if alt_start is not None else (start_char or 0)) + + +def _label_replacement_end(label: dict) -> int: + """ + Determines the end character index for a label, considering possible alternative attributes. + + Args: + label (dict): Label dictionary which may contain alternative end character attributes. + + Returns: + int: The end character index for the label. + """ + attrs = label.get("attrs") or {} + alt_end = attrs.get("aymurai_alt_end_char") + end_char = label.get("end_char") + return int(alt_end if alt_end is not None else (end_char or 0)) + + +def _label_replacement_text(label: dict, document: str) -> str: + """ + Determines the replacement text for a label, considering possible alternative attributes. + + Args: + label (dict): Label dictionary which may contain alternative text attributes. + document (str): The document text from which to extract the label text. + + Returns: + str: The text for the label, considering possible alternative attributes. + """ + attrs = label.get("attrs") or {} + + alt_text = attrs.get("aymurai_alt_text") + if alt_text is not None: + return str(alt_text) if alt_text else "" + + alt_start = attrs.get("aymurai_alt_start_char") + alt_end = attrs.get("aymurai_alt_end_char") + if alt_start is not None and alt_end is not None: + start_char, end_char = int(alt_start), int(alt_end) + if 0 <= start_char < end_char <= len(document): + return document[start_char:end_char] + + start_char = int(label.get("start_char") or 0) + end_char = int(label.get("end_char") or 0) + if 0 <= start_char < end_char <= len(document): + return document[start_char:end_char] + + text = label.get("text") + return str(text) if text else "" + + +def unify_consecutive_labels( + sample: dict, + text_key: str = "document", + render_context: dict | None = None, +) -> list[dict]: + """ + Unifies consecutive labels in a sample. + + Args: + sample (dict): A dictionary representing the sample. + text_key (str, optional): The key for the text in the sample dictionary. + Defaults to "document". + render_context (dict | None, optional): The context for rendering labels. Defaults to None. + + Returns: + list[dict]: A list of dictionaries representing the unified labels. + """ + sample = deepcopy(sample) + + # Extract labels and document text + labels = sample["labels"] + document = sample[text_key] + + # Reorder labels based on start indices + labels = sorted(labels, key=lambda x: x["start_char"]) + + unified_labels = [] + current_group = None + + # Iterate over labels + for label in labels: + # Get attributes + text = _label_replacement_text(label, document) + start_char = _label_replacement_start(label) + end_char = _label_replacement_end(label) + if not text or end_char <= start_char: + continue + aymurai_label = resolve_render_token(label, render_context) + + if current_group is None: + # Start a new group with the current label + current_group = { + "text": text, + "start_char": start_char, + "end_char": end_char, + "aymurai_label": aymurai_label, + } + elif ( + current_group["aymurai_label"] == aymurai_label + and (start_char - current_group["end_char"]) <= 1 + ): + # Extend the current group with the current label + current_group["end_char"] = end_char + else: + # Finish the current group and start a new one + current_group["text"] = document[ + current_group["start_char"] : current_group["end_char"] + ] + unified_labels.append(current_group) + current_group = { + "text": text, + "start_char": start_char, + "end_char": end_char, + "aymurai_label": aymurai_label, + } + + # Finish the last group + if current_group is not None: + current_group["text"] = document[ + current_group["start_char"] : current_group["end_char"] + ] + unified_labels.append(current_group) + + return unified_labels + + +def replace_labels_in_text( + pred: dict, + text_key: str = "document", + render_context: dict | None = None, +) -> str: + """ + Replaces labels in the text with anonymized tokens. + + Args: + pred (dict): A dictionary representing the prediction. + text_key (str, optional): The key for the text in the prediction dictionary. + Defaults to "document". + render_context (dict | None, optional): The context for rendering labels. Defaults to None. + Returns: + str: The text with replaced labels. + """ + pred = deepcopy(pred) + doc = pred[text_key] + + # Unify consecutive labels + unified_labels = unify_consecutive_labels( + pred, render_context=render_context, text_key=text_key + ) + + # Initialize the offset + offset = 0 + + # Replace labels in the text + for unified_label in unified_labels: + # Adjust start and end character indices of the label + start_char = unified_label["start_char"] + offset + end_char = unified_label["end_char"] + offset + len_text_to_replace = end_char - start_char + + # Replace the text with the anonymized token + aymurai_label = f" <{unified_label['aymurai_label']}>" + len_aymurai_label = len(aymurai_label) + + doc = doc[:start_char] + aymurai_label + doc[end_char:] + + # Update the offset + offset += len_aymurai_label - len_text_to_replace + + return re.sub(r" +", " ", doc).strip() + + +def erase_duplicates_justseen(series: pd.Series) -> pd.Series: + """ + Replaces consecutive duplicate values in a pandas Series with an empty string, keeping only the first occurrence. + + Args: + series (pd.Series): The input pandas Series. + + Returns: + pd.Series: A pandas Series with consecutive duplicates replaced by an empty string. + """ + return series.where(series.ne(series.shift(), fill_value=None), "") + + +def parse_token_indices( + sample: dict, render_context: dict | None = None +) -> pd.DataFrame: + """ + Parses the token indices from a sample. + + Args: + sample (dict): A dictionary representing the sample. + render_context (dict | None, optional): The context for rendering labels. Defaults to None. + + Returns: + pd.DataFrame: A pandas DataFrame representing the parsed token indices. + """ + original_text = " ".join( + [fragment["text"] for fragment in sample["metadata"]["fragments"]] + ) + anonymized_text = replace_labels_in_text(sample, render_context=render_context) + + aligned = align_text( + " " + original_text + " ", + " " + anonymized_text + " ", + ) + aligned["target"] = erase_duplicates_justseen(aligned["target"]) + + xml_file = sample["metadata"]["xml_file"] + + tokens = [] + for i, fragment in enumerate(sample["metadata"]["fragments"]): + text = fragment["text"] + tokenized_text = tokenize(text) + paragraph_index = fragment["paragraph_index"] + + # Use re.finditer to locate each instance of tokens in the text + token_matches = list( + re.finditer(r"\S+", text) + ) # \S+ matches any non-whitespace sequence + + # Loop over tokenized text and token_matches in parallel + for j, (token, match) in enumerate(zip(tokenized_text, token_matches)): + start = sample["metadata"]["start"] + fragment["start"] + match.start() + end = start + len(token) + + tokens.append((xml_file, paragraph_index, i, j, token, start, end)) + + tokens = pd.DataFrame( + tokens, + columns=[ + "xml_file", + "paragraph_index", + "fragment_index", + "token_index", + "token", + "start_char", + "end_char", + ], + ) + + tokens = pd.concat( + [tokens, aligned["target"].iloc[1:-1].reset_index(drop=True)], axis=1 + ) + + tokens["target"] = tokens["target"].fillna("") + + return tokens + + +def index_paragraphs(file: str) -> list[dict]: + """ + Indexes the paragraphs of an XML file. + + Args: + file (str): The path to the XML file to be indexed. + + Returns: + list[dict]: A list of dictionaries representing the indexed paragraphs. + """ + # Read the XML file + with open(file, encoding="utf-8-sig") as f: + xml = f.read() + + paragraphs = [] + paragraph_index = 0 + + # Find all paragraphs in the XML file + for match in re.finditer(REGEX_PARAGRAPH, xml): + paragraph = match.group("paragraph") + paragraph_start = match.start("paragraph") + paragraph_end = match.end("paragraph") + fragments = [] + fragment_index = 0 + + # Find all text fragments in the paragraph + for fragment in re.finditer(REGEX_FRAGMENT, paragraph): + text = fragment.group("text") + start = fragment.start("text") + end = fragment.end("text") + + fragment_dict = { + "text": text, + "normalized_text": FlairTextNormalize.normalize_text(text), + "start": start, + "end": end, + "fragment_index": fragment_index, + "paragraph_index": paragraph_index, + } + fragments.append(fragment_dict) + fragment_index += 1 + + # Join all fragments as plain text + plain_text = "".join([fragment["normalized_text"] for fragment in fragments]) + + paragraphs.append( + { + "plain_text": plain_text, + "metadata": { + "start": paragraph_start, + "end": paragraph_end, + "fragments": fragments, + "xml_file": os.path.basename(file), + }, + } + ) + paragraph_index += 1 + + return paragraphs + + +def match_paragraphs_with_predictions( + paragraphs: list[dict], + predictions: list[dict], +) -> list[dict]: + """ + Matches each paragraph with its corresponding predictions. + + Args: + paragraphs (list[dict]): A list of dictionaries representing the paragraphs. + predictions (list[dict]): A list of dictionaries representing + the predictions. + + Returns: + list[dict]: A list of dictionaries representing + the matched paragraphs with predictions. + """ + + paragraphs = deepcopy(paragraphs) + + # Hash prediction documents + pred_hashes = [hash(prediction["document"]) for prediction in predictions] + idx2hash = {i: _hash for i, _hash in enumerate(pred_hashes)} + + # Hash paragraphs + paragraphs = [ + paragraph | {"hash": hash(normalize("NFKC", paragraph["plain_text"].strip()))} + for paragraph in paragraphs + ] + + # Assign prediction indices to each paragraph by hash + hash2idx = { + paragraph["hash"]: np.where(np.array(pred_hashes) == paragraph["hash"])[ + 0 + ].tolist() + for paragraph in paragraphs + } + + paragraphs = [ + paragraph | {"pred_indices": hash2idx[paragraph["hash"]]} + for paragraph in paragraphs + ] + + # Identify missing indices + missing_indices = list(set(idx2hash.keys()) - set(flatten(list(hash2idx.values())))) + + if missing_indices: + # Assign prediction indices to each paragraph by lowest CER + target_texts = np.array([prediction["document"] for prediction in predictions])[ + missing_indices + ] + + missing_paragraphs = [ + paragraph for paragraph in paragraphs if not paragraph["pred_indices"] + ] + + for missing_paragraph in missing_paragraphs: + source_text = missing_paragraph["plain_text"] + min_cer_idx = np.argmin( + [cer(source_text, target_text) for target_text in target_texts] + ) + missing_paragraph["pred_indices"] = [missing_indices[min_cer_idx]] + + # Assign document text and labels + paragraphs = [ + paragraph + | { + "document": predictions[paragraph["pred_indices"][0]]["document"], + "labels": predictions[paragraph["pred_indices"][0]]["labels"], + } + for paragraph in paragraphs + ] + + return paragraphs diff --git a/aymurai/text/anonymization/base.py b/aymurai/text/anonymization/base.py new file mode 100644 index 00000000..a1631159 --- /dev/null +++ b/aymurai/text/anonymization/base.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Any + + +class InvalidDocumentAnonymizer(Exception): + """Raised when an anonymizer receives an invalid or unsupported document.""" + + +class BaseAnonymizer(ABC): + """Common interface shared by all document anonymizers.""" + + extension: str + + @property + def __name__(self) -> str: + return self.__class__.__name__ + + def ensure_file(self, path: Path) -> Path: + if not path.exists(): + raise InvalidDocumentAnonymizer(f"Invalid path: {path}") + return path + + def __call__( + self, + item: dict, + preds: list[dict], + output_dir: str = ".", + render_context: dict[str, Any] | None = None, + ) -> str: + return self.anonymize(item, preds, output_dir, render_context=render_context) + + @abstractmethod + def anonymize( + self, + item: dict, + preds: list[dict], + output_dir: str = ".", + render_context: dict[str, Any] | None = None, + ) -> str: + """Anonymize a document and return the output path.""" + + +_REGISTRY: dict[str, type[BaseAnonymizer]] = {} + + +def register_anonymizer(cls: type[BaseAnonymizer]) -> type[BaseAnonymizer]: + extension = getattr(cls, "extension", None) + if not extension: + raise ValueError( + f"Anonymizer {cls.__name__} must define an 'extension' attribute" + ) + + _REGISTRY[extension.lower()] = cls + return cls + + +def get_anonymizer(extension: str) -> BaseAnonymizer: + normalized = extension.lower() + try: + anonymizer_cls = _REGISTRY[normalized] + except KeyError as exc: + raise ValueError(f"Unsupported extension: {extension}") from exc + return anonymizer_cls() + + +def supported_extensions() -> set[str]: + return set(_REGISTRY.keys()) + + +__all__ = [ + "BaseAnonymizer", + "InvalidDocumentAnonymizer", + "get_anonymizer", + "register_anonymizer", + "supported_extensions", +] diff --git a/aymurai/text/anonymization/docx/__init__.py b/aymurai/text/anonymization/docx/__init__.py new file mode 100644 index 00000000..5d5d0aca --- /dev/null +++ b/aymurai/text/anonymization/docx/__init__.py @@ -0,0 +1,3 @@ +from aymurai.text.anonymization.docx.anonymizer import DocxAnonymizer + +__all__ = ["DocxAnonymizer"] diff --git a/aymurai/text/anonymization/docx/anonymizer.py b/aymurai/text/anonymization/docx/anonymizer.py new file mode 100644 index 00000000..73c43487 --- /dev/null +++ b/aymurai/text/anonymization/docx/anonymizer.py @@ -0,0 +1,122 @@ +import os +import tempfile +from datetime import datetime, timezone +from glob import glob +from pathlib import Path +from typing import Any + +from docx import Document +from more_itertools import flatten + +from aymurai.text.anonymization.alignment import ( + index_paragraphs, + match_paragraphs_with_predictions, +) +from aymurai.text.anonymization.base import ( + BaseAnonymizer, + InvalidDocumentAnonymizer, + register_anonymizer, +) +from aymurai.text.anonymization.docx.watermark import add_footer_watermark +from aymurai.settings import settings +from aymurai.text.anonymization.docx.xml import ( + create_docx, + replace_text_in_xml, + unzip_document, +) +from aymurai.utils.cache import cache_load, cache_save, get_cache_key + + +def _set_aymurai_core_properties(doc_path: str) -> None: + """ + Applies the configured AymurAI tooling metadata fields to the DOCX core properties. + + Args: + doc_path (str): The path to the DOCX document to update. + """ + document = Document(doc_path) + core_properties = document.core_properties + core_properties.author = "" + core_properties.last_modified_by = settings.ANONYMIZATION_METADATA_CREATOR + core_properties.modified = datetime.now(timezone.utc) + document.save(doc_path) + + +@register_anonymizer +class DocxAnonymizer(BaseAnonymizer): + """ + Anonymize DOCX documents by replacing sensitive data with label tokens. + """ + + extension = "docx" + + def __init__(self, use_cache: bool = False): + self.use_cache = use_cache + + def anonymize( + self, + item: dict, + preds: list[dict], + output_dir: str = ".", + render_context: dict[str, Any] | None = None, + ) -> str: + """ + Anonymizes a DOCX document using the matched paragraph predictions. + + Args: + item (dict): The item dictionary containing the input DOCX path. + preds (list[dict]): The predictions to apply to the document. + output_dir (str, optional): The directory where the anonymized document should be written. Defaults to '.'. + render_context (dict[str, Any] | None, optional): The rendering context used to resolve replacement tokens. + Defaults to None. + + Returns: + str: The path to the anonymized DOCX output file. + """ + item_path = Path(item["path"]) + file_path = self.ensure_file(item_path) + + if file_path.suffix.lower() != ".docx": + raise InvalidDocumentAnonymizer("Only `.docx` extension is allowed.") + + if not item.get("data"): + item["data"] = {} + + cache_key = get_cache_key(str(file_path), self.__name__) + if self.use_cache and (cache_data := cache_load(key=cache_key)): + paragraphs = cache_data + else: + # Unzip document into a temporary directory + with tempfile.TemporaryDirectory() as tempdir: + unzip_document(str(file_path), tempdir) + + # Parse XML files + xml_files = glob(f"{tempdir}/**/*.xml", recursive=True) + paragraphs = (index_paragraphs(file) for file in xml_files) + paragraphs = list(flatten(paragraphs)) + + # Filter out empty paragraphs + paragraphs = [ + paragraph + for paragraph in paragraphs + if paragraph["plain_text"].strip() + ] + # Matching + paragraphs = match_paragraphs_with_predictions(paragraphs, preds) + + # Edit XML files + replace_text_in_xml(paragraphs, tempdir, render_context) + + # Recreate anonymized document + os.makedirs(output_dir, exist_ok=True) + output_path = f"{output_dir}/{os.path.basename(str(file_path))}" + create_docx(tempdir, output_path) + + # Add metadata branding and the footer watermark + _set_aymurai_core_properties(output_path) + add_footer_watermark(output_path) + + if self.use_cache: + cache_save(paragraphs, key=cache_key) + + return f"{output_dir}/{os.path.basename(str(file_path))}" diff --git a/aymurai/text/anonymization/docx/watermark.py b/aymurai/text/anonymization/docx/watermark.py new file mode 100644 index 00000000..3199bb9e --- /dev/null +++ b/aymurai/text/anonymization/docx/watermark.py @@ -0,0 +1,197 @@ +from docx import Document +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.opc.constants import RELATIONSHIP_TYPE +from docx.oxml.shared import OxmlElement, qn +from docx.shared import Inches, Pt, RGBColor + + +def _add_hyperlink( + paragraph, + text: str, + url: str, + font_name: str = "Archivo", + size: int = 10, + color: RGBColor = RGBColor(115, 190, 250), + italic: bool = False, + bold: bool = True, + underline: bool = True, +) -> None: + """ + Adds a formatted hyperlink to a given paragraph in a Word document. + + Notes: + - This method directly manipulates the underlying XML of the paragraph to insert a hyperlink, + as python-docx does not natively support hyperlinks. + - The hyperlink will be appended to the end of the given paragraph. + - Formatting options (font, size, color, italic, bold, underline) are applied to the hyperlink text. + + Args: + paragraph: The python-docx paragraph object to which the hyperlink will be added. + text (str): The display text for the hyperlink. + url (str): The URL that the hyperlink points to. + font_name (str, optional): The font name to use for the hyperlink text. Defaults to "Archivo". + size (int, optional): The font size (in points) for the hyperlink text. Defaults to 10. + color (RGBColor, optional): The font color as an RGBColor tuple. Defaults to RGBColor(115, 190, 250). + italic (bool, optional): Whether the hyperlink text should be italicized. Defaults to False. + bold (bool, optional): Whether the hyperlink text should be bold. Defaults to True. + underline (bool, optional): Whether the hyperlink text should be underlined. Defaults to True. + + Raises: + ValueError: If the paragraph is not a valid python-docx paragraph object. + """ + # Create the hyperlink relationship + part = paragraph.part + r_id = part.relate_to(url, RELATIONSHIP_TYPE.HYPERLINK, is_external=True) + + # Create the hyperlink element + hyperlink = OxmlElement("w:hyperlink") + hyperlink.set(qn("r:id"), r_id) + + # Create a new run + new_run = OxmlElement("w:r") + + # Set run properties (formatting) + r_pr = OxmlElement("w:rPr") + + # Set font + if font_name: + font = OxmlElement("w:rFonts") + font.set(qn("w:ascii"), font_name) + font.set(qn("w:hAnsi"), font_name) + r_pr.append(font) + + # Set color + if color: + color_el = OxmlElement("w:color") + color_el.set(qn("w:val"), f"{color[0]:02x}{color[1]:02x}{color[2]:02x}") + r_pr.append(color_el) + + # Set italic + if italic: + r_pr.append(OxmlElement("w:i")) + + # Set bold + if bold: + r_pr.append(OxmlElement("w:b")) + + # Set underline - added for links + if underline: + underline_el = OxmlElement("w:u") + underline_el.set(qn("w:val"), "single") # single underline + r_pr.append(underline_el) + + # Set size + sz = OxmlElement("w:sz") + sz.set(qn("w:val"), str(size * 2)) # Word uses half-points + r_pr.append(sz) + + # Add properties to run + new_run.append(r_pr) + + # Set text + text_el = OxmlElement("w:t") + text_el.text = text + new_run.append(text_el) + + # Add run to hyperlink + hyperlink.append(new_run) + + # Add hyperlink to paragraph + paragraph._p.append(hyperlink) + + +def _add_watermark_to_footer( + footer, + alignment, + font_name: str = "Archivo", + hyperlink_text: str = "AymurAI", + hyperlink_url: str = "https://www.aymurai.info/", + watermark_text: str = "Documento anonimizado por AymurAI", +) -> None: + """ + Adds a watermark text to the footer of a document. + + Args: + footer: The footer object to which the watermark will be added. + alignment: The alignment setting for the paragraph (e.g., left, center, right). + font_name (str, optional): The font name to use for the watermark text. Defaults to "Archivo". + hyperlink_text (str, optional): The text to be hyperlinked. Defaults to "AymurAI". + hyperlink_url (str, optional): The URL to link "AymurAI" to. Defaults to "https://www.aymurai.info/". + watermark_text (str): The text to be used as the watermark. Defaults to "Documento anonimizado por AymurAI". + """ + paragraph = footer.add_paragraph() + paragraph.alignment = alignment + + if hyperlink_url and hyperlink_text in watermark_text: + parts = watermark_text.split(hyperlink_text, 1) + before_text = parts[0] + after_text = parts[1] if len(parts) > 1 else "" + + # Add text before the hyperlink + if before_text: + run = paragraph.add_run(before_text) + run.font.name = "Archivo" + run.font.color.rgb = RGBColor(192, 192, 192) + run.font.size = Pt(10) + + # Add hyperlink + _add_hyperlink(paragraph, hyperlink_text, hyperlink_url) + + # Add text after the hyperlink + if after_text: + run = paragraph.add_run(after_text) + run.font.name = font_name + run.font.color.rgb = RGBColor(192, 192, 192) + run.font.size = Pt(10) + + else: + # Just add the full text without a hyperlink + run = paragraph.add_run(watermark_text) + run.font.name = font_name + run.font.color.rgb = RGBColor(192, 192, 192) + run.font.size = Pt(10) + + +def add_footer_watermark( + doc_path: str, + font_name: str = "Archivo", + hyperlink_text: str = "AymurAI", + hyperlink_url: str = "https://www.aymurai.info/", + watermark_text: str = "Documento anonimizado por AymurAI", +) -> None: + """ + Adds a watermark to the footer of each section in a Word document. + + Args: + doc_path (str): Path to the document. + font_name (str, optional): The font name to use for the watermark text. Defaults to "Archivo". + hyperlink_text (str, optional): The text to be hyperlinked. Defaults to "AymurAI". + hyperlink_url (str, optional): The URL to link "AymurAI" to. Defaults to "https://www.aymurai.info/". + watermark_text (str, optional): The text to be used as the watermark. Defaults to "Documento anonimizado por AymurAI". + """ + document = Document(doc_path) + processed_footers = set() + + for section in document.sections: + section.footer_distance = Inches(0.1) + + # List of (footer_obj, alignment) tuples + footers = [(section.footer, WD_ALIGN_PARAGRAPH.RIGHT)] # Odd/default + if section.even_page_footer is not None: + footers.append((section.even_page_footer, WD_ALIGN_PARAGRAPH.LEFT)) + if section.different_first_page_header_footer: + footers.append((section.first_page_footer, WD_ALIGN_PARAGRAPH.RIGHT)) + + for footer, alignment in footers: + if id(footer) not in processed_footers: + _add_watermark_to_footer( + footer, + alignment, + font_name=font_name, + hyperlink_text=hyperlink_text, + hyperlink_url=hyperlink_url, + watermark_text=watermark_text, + ) + processed_footers.add(id(footer)) + + document.save(doc_path) diff --git a/aymurai/text/anonymization/docx/xml.py b/aymurai/text/anonymization/docx/xml.py new file mode 100644 index 00000000..1ae9f50e --- /dev/null +++ b/aymurai/text/anonymization/docx/xml.py @@ -0,0 +1,172 @@ +import os +import re +import xml.sax.saxutils +import zipfile + +import pandas as pd +from lxml import etree + +from aymurai.text.anonymization.alignment import parse_token_indices + + +def unzip_document(doc_path: str, output_dir: str) -> None: + """ + Unzips the document file to the specified output directory. + + Args: + doc_path (str): The path to the document file. + output_dir (str): The directory where the contents of the document + file will be extracted. + """ + # Ensure the output directory exists + os.makedirs(output_dir, exist_ok=True) + + # Open the doc file as a zip file + with zipfile.ZipFile(doc_path, "r") as doc_zip: + # Extract all the contents to the output directory + doc_zip.extractall(output_dir) + + +def normalize_document(xml_content: str) -> str: + """ + Normalizes the XML document by removing extra spaces, preserving line breaks, + and removing hyperlinks while preserving text content. + + Args: + xml_content (str): The XML content to be normalized. + + Returns: + str: The normalized XML content. + """ + # Parse the XML content + parser = etree.XMLParser(ns_clean=True) + root = etree.fromstring(xml_content.encode("utf-8"), parser) + + # Extract namespaces + namespaces = {k: v for k, v in root.nsmap.items() if k} + + # Remove hyperlinks but preserve the text content + for hyperlink in root.xpath("//w:hyperlink", namespaces=namespaces): + parent = hyperlink.getparent() + index = list(parent).index(hyperlink) + + # Move all text-containing children (e.g., w:r elements) outside the hyperlink tag + for child in hyperlink: + parent.insert(index, child) + index += 1 + parent.remove(hyperlink) # Remove the element itself + + # Process each paragraph + for wp in root.xpath("//w:p", namespaces=namespaces): + first = True + + # Find all w:r elements containing w:t elements + for wr in wp.xpath(".//w:r", namespaces=namespaces): + wt = wr.find(".//w:t", namespaces) + if wt is not None and wt.text: + # Normalize spaces within the text, preserving new line breaks + wt.text = re.sub(r"[^\S\r\n]+", " ", wt.text) + + # Add a leading space if not the first fragment in the paragraph + if not first: + wt.text = " " + wt.text.lstrip() + else: + wt.text = wt.text.lstrip() + first = False + + # Remove trailing spaces from all fragments + wt.text = wt.text.rstrip() + + # Set the xml:space attribute to preserve + wt.set( + "{http://www.w3.org/XML/1998/namespace}space", + "preserve", + ) + + # Check if the text is empty after normalization + if not wt.text or wt.text.strip() == "": + # Remove the w:r element from its parent + wr.getparent().remove(wr) + + # Write back the XML content to a string + xml_str = etree.tostring(root, encoding="unicode", pretty_print=True) + + return xml_str + + +def replace_text_in_xml( + paragraphs: list[dict], base_dir: str, render_context: dict | None = None +) -> None: + """ + Replaces text in XML files based on the provided paragraphs + and saves the modified files. + + Args: + paragraphs (list[dict]): A list of dictionaries representing + the paragraphs to be replaced. + base_dir (str): The base directory where the XML files are located. + render_context (dict | None, optional): The context for rendering labels. Defaults to None. + """ + tokens = pd.concat( + [parse_token_indices(sample, render_context) for sample in paragraphs], + ignore_index=True, + ) + + fragments = ( + tokens.groupby(["xml_file", "paragraph_index", "fragment_index"]) + .agg({"target": " ".join, "start_char": "min", "end_char": "max"}) + .reset_index() + ) + + for xml_file, group in fragments.groupby("xml_file"): + group = group.sort_values("end_char", ascending=False) + + with open(f"{base_dir}/word/{xml_file}", "r+") as file: + content = file.read() + + for _, r in group.iterrows(): + start_char = r["start_char"] + end_char = r["end_char"] + + target = r["target"] + target = re.sub(r"[^\S\r\n]+", " ", target) + + # Escape XML special characters + target = xml.sax.saxutils.escape(target) + + content = content[:start_char] + target + content[end_char:] + + # MUST be at the end to dont screw up the indexes + content = normalize_document(content) + + file.seek(0) + file.write(content) + file.truncate() + + +def add_files_to_zip(zip_file: zipfile.ZipFile, directory: str) -> None: + """ + Adds all files in the specified directory to a zip file. + + Args: + zip_file (zipfile.ZipFile): The zip file to add the files to. + directory (str): The directory containing the files to be added. + """ + for root, _, files in os.walk(directory): + for file in files: + file_path = os.path.join(root, file) + zip_file.write(file_path, os.path.relpath(file_path, directory)) + + +def create_docx(xml_directory: str, output_file: str) -> None: + """ + Creates a new DOCX file by adding XML components from the specified directory. + + Args: + xml_directory (str): The directory containing the XML components. + output_file (str): The path to the output DOCX file. + """ + # Create a new zip file + with zipfile.ZipFile(output_file, "w") as docx: + # Add XML components + add_files_to_zip(docx, xml_directory) diff --git a/aymurai/text/anonymization/pdf/__init__.py b/aymurai/text/anonymization/pdf/__init__.py new file mode 100644 index 00000000..21271aae --- /dev/null +++ b/aymurai/text/anonymization/pdf/__init__.py @@ -0,0 +1,3 @@ +from aymurai.text.anonymization.pdf.anonymizer import PdfAnonymizer + +__all__ = ["PdfAnonymizer"] diff --git a/aymurai/text/anonymization/pdf/anonymizer.py b/aymurai/text/anonymization/pdf/anonymizer.py new file mode 100644 index 00000000..0030c24b --- /dev/null +++ b/aymurai/text/anonymization/pdf/anonymizer.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any + +import pymupdf +import pymupdf.layout # noqa: F401 # activates layout support +from pymupdf4llm.helpers import document_layout as pymupdf4llm_document_layout + +from aymurai.text.anonymization.base import ( + BaseAnonymizer, + InvalidDocumentAnonymizer, + register_anonymizer, +) +from aymurai.text.anonymization.pdf.layout import ( + _apply_minimal_boundary_merge, + _build_layout_paragraphs, + _match_predictions_to_layout, +) +from aymurai.text.anonymization.pdf.ops import ( + _apply_redactions, + _collect_page_redactions, +) +from aymurai.text.anonymization.pdf.sanitize import ( + _collect_link_cleanup_rects, + _sanitize_document, +) +from aymurai.text.anonymization.pdf.watermark import add_pdf_footer_watermark + + +@register_anonymizer +class PdfAnonymizer(BaseAnonymizer): + """ + Anonymize PDF documents by replacing sensitive data with label tokens. + """ + + extension = "pdf" + + def anonymize( + self, + item: dict, + preds: list[dict], + output_dir: str = ".", + render_context: dict[str, Any] | None = None, + ) -> str: + """ + Anonymizes a PDF document using the matched paragraph predictions. + + Args: + item (dict): The item dictionary containing the input PDF path. + preds (list[dict]): The predictions to apply to the document. + output_dir (str, optional): The directory where the anonymized document should be written. Defaults to '.'. + render_context (dict[str, Any] | None, optional): The rendering context used to resolve replacement tokens. Defaults to None. + + Returns: + str: The path to the anonymized PDF output file. + """ + item_path = Path(item["path"]) + file_path = self.ensure_file(item_path) + + if file_path.suffix.lower() != ".pdf": + raise InvalidDocumentAnonymizer("Only `.pdf` extension is allowed.") + + with pymupdf.open(str(file_path)) as doc: + parsed_doc = pymupdf4llm_document_layout.parse_document( + doc, + filename=str(file_path), + show_progress=False, + force_text=True, + use_ocr=False, + force_ocr=False, + ) + + layout_paragraphs = _build_layout_paragraphs(parsed_doc) + matched_paragraphs = _match_predictions_to_layout( + layout_paragraphs, + preds, + ) + + _apply_minimal_boundary_merge(matched_paragraphs, render_context) + page_ops, widget_ops, signature_widget_ops = _collect_page_redactions( + doc, + matched_paragraphs, + render_context, + ) + _apply_redactions(doc, page_ops, widget_ops, signature_widget_ops) + cleanup_rects = _collect_link_cleanup_rects( + page_ops, + widget_ops, + signature_widget_ops, + ) + _sanitize_document(doc, cleanup_rects) + add_pdf_footer_watermark(doc) + + os.makedirs(output_dir, exist_ok=True) + output_path = Path(output_dir) / f"{file_path.stem}.anonymized.pdf" + doc.save(str(output_path), garbage=4, clean=1, deflate=1) + + return str(output_path) diff --git a/aymurai/text/anonymization/pdf/common.py b/aymurai/text/anonymization/pdf/common.py new file mode 100644 index 00000000..b17c22c2 --- /dev/null +++ b/aymurai/text/anonymization/pdf/common.py @@ -0,0 +1,620 @@ +from __future__ import annotations + +import re +from functools import lru_cache +from typing import Any +from unicodedata import normalize + +import pymupdf + +TEXT_FLAG_ITALIC = 2 +TEXT_FLAG_SERIF = 4 +TEXT_FLAG_MONOSPACED = 8 +TEXT_FLAG_BOLD = 16 +PDF_TAG_MIN_FONT_SIZE = 7.0 +PDF_TAG_FONT_STEP = 0.5 +PDF_TAG_MAX_ABBREVIATION = 3 +PDF_TOKEN_ALIAS_MAP: dict[str, tuple[str, ...]] = { + "CORREO_ELECTRONICO": ("CORREO", "MAIL"), + "CUIT_CUIL": ("CUIT", "CUIL"), + "DIRECCION": ("DIR",), + "ESTUDIOS": ("EDU",), + "MARCA_AUTOMOVIL": ("VEHICULO", "AUTO"), + "NACIONALIDAD": ("PAIS", "NAC"), + "NOMBRE_ARCHIVO": ("ARCHIVO", "FILE"), + "NUM_ACTUACION": ("ACTUACION", "ACT"), + "NUM_CAJA_AHORRO": ("CAJA_AHORRO", "CAJA"), + "NUM_EXPEDIENTE": ("EXPEDIENTE", "EXPTE"), + "NUM_MATRICULA": ("MATRICULA", "MAT"), + "PATENTE_DOMINIO": ("PATENTE", "DOMINIO"), + "TELEFONO": ("TEL",), + "TEXTO_ANONIMIZAR": ("ANONIMIZAR", "TEXTO"), + "USUARIX": ("USER",), +} +PDF_TAG_RECT_X_PADDING = 0.5 +PDF_TAG_RECT_Y_PADDING = 0.0 +PDF_TAG_RECT_INSET = 0.5 +PDF_TAG_RECT_GAP_FACTOR = 0.5 +PDF_TAG_RECT_GAP_MIN = 3.0 +PDF_TAG_RECT_GAP_MAX = 8.0 + + +def _line_text(line: dict) -> str: + """ + Builds the plain text content for a parsed PDF line. + + Args: + line (dict): The parsed line metadata being processed. + + Returns: + str: The concatenated text content for the line. + """ + return "".join(span.get("text", "") for span in line.get("spans", [])) + + +def _rect_tuple(value: Any) -> tuple[float, float, float, float]: + """ + Normalizes a rectangle-like value into a coordinate tuple. + + Args: + value (Any): The rectangle-like value to normalize. + + Returns: + tuple[float, float, float, float]: The normalized rectangle coordinates. + """ + if isinstance(value, pymupdf.Rect): + return (float(value.x0), float(value.y0), float(value.x1), float(value.y1)) + if isinstance(value, (list, tuple)) and len(value) == 4: + return (float(value[0]), float(value[1]), float(value[2]), float(value[3])) + raise ValueError(f"Invalid rectangle value: {value}") + + +def _default_style(fallback_size: float = 10.0) -> dict[str, Any]: + """ + Builds a default text style dictionary for PDF rendering helpers. + + Args: + fallback_size (float, optional): The fallback font size used when no style data is available. Defaults to 10.0. + + Returns: + dict[str, Any]: The default style dictionary. + """ + return { + "font": "", + "flags": 0, + "color": (0.0, 0.0, 0.0), + "size": fallback_size, + "ascender": 0.8, + "descender": -0.2, + } + + +def _span_text_weight(span: dict) -> tuple[int, float]: + """ + Computes a sorting weight for a span based on text length and size. + + Args: + span (dict): The span metadata being evaluated. + + Returns: + tuple[int, float]: The text-length and size weight for the span. + """ + text = str(span.get("text") or "").strip() + return (len(text), float(span.get("size") or 0.0)) + + +def _pdf_color_from_span(span: dict) -> tuple[float, float, float]: + """ + Converts a span color value into PDF RGB components. + + Args: + span (dict): The span metadata being evaluated. + + Returns: + tuple[float, float, float]: The PDF RGB color components for the span. + """ + try: + return tuple( + float(value) for value in pymupdf.sRGB_to_pdf(int(span.get("color") or 0)) + ) + except Exception: + return (0.0, 0.0, 0.0) + + +def _line_style(line: dict, fallback_size: float = 10.0) -> dict[str, Any]: + """ + Determines the dominant text style for a parsed PDF line. + + Args: + line (dict): The parsed line metadata being processed. + fallback_size (float, optional): The fallback font size used when no style data is available. Defaults to 10.0. + + Returns: + dict[str, Any]: The dominant style dictionary for the line. + """ + spans = [ + span for span in line.get("spans") or [] if str(span.get("text") or "").strip() + ] + if not spans: + return _default_style(fallback_size) + + dominant = max(spans, key=_span_text_weight) + return { + "font": str(dominant.get("font") or ""), + "flags": int(dominant.get("flags") or 0), + "color": _pdf_color_from_span(dominant), + "size": float(dominant.get("size") or fallback_size), + "ascender": float(dominant.get("ascender") or 0.8), + "descender": float(dominant.get("descender") or -0.2), + } + + +def _build_spans_detail(line: dict) -> tuple[list[dict], int]: + """ + Builds per-span style metadata and character offsets for a line. + + Args: + line (dict): The parsed line metadata being processed. + + Returns: + tuple[list[dict], int]: The span detail list and left-strip offset. + """ + raw_text = normalize("NFKC", _line_text(line)) + strip_offset = len(raw_text) - len(raw_text.lstrip()) + + spans_detail: list[dict] = [] + cursor = 0 + for span in line.get("spans", []): + span_text = normalize("NFKC", span.get("text", "")) + span_start = cursor + cursor += len(span_text) + spans_detail.append( + { + "start": span_start, + "end": cursor, + "style": { + "font": str(span.get("font") or ""), + "flags": int(span.get("flags") or 0), + "color": _pdf_color_from_span(span), + "size": float(span.get("size") or 10.0), + "ascender": float(span.get("ascender") or 0.8), + "descender": float(span.get("descender") or -0.2), + }, + } + ) + return spans_detail, strip_offset + + +def _entity_style_from_spans( + line_entry: dict, + offset_in_stripped_text: int, +) -> dict[str, Any]: + """ + Resolves the style for the entity offset inside a line entry. + + Args: + line_entry (dict): The `line_entry` value used by this helper. + offset_in_stripped_text (int): The entity offset inside the stripped line text. + + Returns: + dict[str, Any]: The resolved style dictionary for the entity offset. + """ + spans_detail = line_entry.get("spans_detail") + if not spans_detail: + return line_entry.get("style") or _default_style() + + strip_offset = line_entry.get("strip_offset", 0) + raw_offset = offset_in_stripped_text + strip_offset + + for span_info in spans_detail: + if span_info["start"] <= raw_offset < span_info["end"]: + return span_info["style"] + + return line_entry.get("style") or _default_style() + + +def _font_size(line: dict, fallback: float = 10.0) -> float: + """ + Calculates a representative font size for a parsed line. + + Args: + line (dict): The parsed line metadata being processed. + fallback (float, optional): The fallback font size to use when the line has no span sizes. Defaults to 10.0. + + Returns: + float: The representative font size for the line. + """ + spans = line.get("spans") or [] + sizes = [float(span.get("size")) for span in spans if span.get("size")] + if not sizes: + return fallback + size = sum(sizes) / len(sizes) + return max(size * 0.9, PDF_TAG_MIN_FONT_SIZE) + + +def _style_flags(style: dict[str, Any]) -> tuple[bool, bool, bool, bool]: + """ + Extracts boolean style flags from a style dictionary. + + Args: + style (dict[str, Any]): The style dictionary being analyzed. + + Returns: + tuple[bool, bool, bool, bool]: The bold, italic, monospace, and serif flags. + """ + flags = int(style.get("flags") or 0) + font_label = str(style.get("font") or "").lower() + + is_bold = bool(flags & TEXT_FLAG_BOLD) or "bold" in font_label + is_italic = bool(flags & TEXT_FLAG_ITALIC) or any( + token in font_label for token in ("italic", "oblique") + ) + is_mono = bool(flags & TEXT_FLAG_MONOSPACED) or any( + token in font_label for token in ("courier", "mono", "console") + ) + is_serif = bool(flags & TEXT_FLAG_SERIF) or any( + token in font_label + for token in ("times", "serif", "georgia", "garamond", "mistral") + ) + return is_bold, is_italic, is_mono, is_serif + + +def _base14_fontname_for_style(style: dict[str, Any]) -> str: + """ + Maps a style dictionary to the closest Base-14 font name. + + Args: + style (dict[str, Any]): The style dictionary being analyzed. + + Returns: + str: The Base-14 font name that best matches the style. + """ + is_bold, is_italic, is_mono, is_serif = _style_flags(style) + + if is_mono: + family = "Courier" + elif is_serif: + family = "Times" + else: + family = "Helvetica" + + variants = { + ("Helvetica", False, False): "Helvetica", + ("Helvetica", True, False): "Helvetica-Bold", + ("Helvetica", False, True): "Helvetica-Oblique", + ("Helvetica", True, True): "Helvetica-BoldOblique", + ("Times", False, False): "Times-Roman", + ("Times", True, False): "Times-Bold", + ("Times", False, True): "Times-Italic", + ("Times", True, True): "Times-BoldItalic", + ("Courier", False, False): "Courier", + ("Courier", True, False): "Courier-Bold", + ("Courier", False, True): "Courier-Oblique", + ("Courier", True, True): "Courier-BoldOblique", + } + return variants[(family, is_bold, is_italic)] + + +def _build_flexible_pattern(text: str) -> str: + """ + Builds a whitespace-tolerant regex pattern for the given text. + + Args: + text (str): The text value being normalized or searched. + + Returns: + str: The whitespace-tolerant regex pattern. + """ + tokens = [re.escape(tok) for tok in re.split(r"\s+", text.strip()) if tok] + return r"\s+".join(tokens) + + +def _find_flexible( + haystack: str, + needle: str, + start: int = 0, +) -> tuple[int, int] | None: + """ + Finds a text span using exact and whitespace-tolerant matching. + + Args: + haystack (str): The source text to search within. + needle (str): The target text to search for. + start (int, optional): The preferred start offset for the search. Defaults to 0. + + Returns: + tuple[int, int] | None: The start and end offsets of the match, if found. + """ + if not needle: + return None + + idx = haystack.find(needle, start) + if idx >= 0: + return idx, idx + len(needle) + + pattern = _build_flexible_pattern(needle) + if not pattern: + return None + + match = re.search(pattern, haystack[start:]) + if match: + return start + match.start(), start + match.end() + + if start > 0: + match = re.search(pattern, haystack) + if match: + return match.start(), match.end() + + return None + + +def _token_parts(token: str) -> tuple[str, str | None]: + """ + Splits a logical token into its base label and numeric suffix. + + Args: + token (str): The logical replacement token being processed. + + Returns: + tuple[str, str | None]: The token base and optional numeric suffix. + """ + match = re.match(r"^(.*?)(?:_(\d+))?$", token) + if not match: + normalized = token.strip() or "ENT" + return normalized, None + + base = match.group(1).strip() or "ENT" + suffix = match.group(2) + return base, suffix + + +def _abbreviate_token(base: str, length: int) -> str: + """ + Builds an abbreviated token label with the requested length. + + Args: + base (str): The token base label to abbreviate or alias. + length (int): The target abbreviation length. + + Returns: + str: The abbreviated token label. + """ + normalized = "".join(char for char in base.upper() if char.isalnum()) + if not normalized: + normalized = "ENT" + return normalized[:length] or normalized[:1] or "E" + + +def _token_aliases(base: str) -> tuple[str, ...]: + """ + Returns configured alias labels for a token base. + + Args: + base (str): The token base label to abbreviate or alias. + + Returns: + tuple[str, ...]: The configured aliases for the token base. + """ + aliases = PDF_TOKEN_ALIAS_MAP.get(base.upper(), ()) + normalized_aliases: list[str] = [] + + for alias in aliases: + normalized = re.sub(r"[^A-Z0-9_]", "", str(alias).upper()) + if ( + normalized + and normalized != base.upper() + and normalized not in normalized_aliases + ): + normalized_aliases.append(normalized) + + return tuple(normalized_aliases) + + +def _build_display_token_candidates(token: str) -> list[str]: + """ + Builds the list of token display candidates to try when rendering. + + Args: + token (str): The logical replacement token being processed. + + Returns: + list[str]: The candidate display tokens to try when rendering. + """ + base, suffix = _token_parts(token.upper()) + candidates: list[str] = [] + + def add(value: str) -> None: + """ + Appends a token display candidate when it has not been added yet. + + Args: + value (str): The rectangle-like value to normalize. + """ + if value and value not in candidates: + candidates.append(value) + + def add_base_variants(label: str) -> None: + """ + Appends the base token variants for the current label candidate. + + Args: + label (str): The label metadata being processed. + """ + if suffix: + add(f"<{label}_{suffix}>") + add(f"<{label}>") + + add_base_variants(base) + + for alias in _token_aliases(base): + add_base_variants(alias) + + abbreviated = _abbreviate_token(base, PDF_TAG_MAX_ABBREVIATION) + add_base_variants(abbreviated) + + return candidates + + +def _iter_font_sizes(start_size: float) -> list[float]: + """ + Builds the descending font sizes to try when fitting a token. + + Args: + start_size (float): The `start_size` value used by this helper. + + Returns: + list[float]: The font sizes to try in descending order. + """ + if start_size <= 0: + return [] + + sizes: list[float] = [start_size] + current = start_size + while current - PDF_TAG_FONT_STEP >= PDF_TAG_MIN_FONT_SIZE - 1e-6: + current = round(current - PDF_TAG_FONT_STEP, 2) + if current not in sizes: + sizes.append(current) + + return sizes + + +def _fit_display_token( + token: str, + rect: pymupdf.Rect, + fontname: str, + base_font_size: float, + font_obj: pymupdf.Font | None = None, +) -> tuple[str | None, float | None]: + """ + Finds a token rendering variant and font size that fit inside a rectangle. + + Args: + token (str): The logical replacement token being processed. + rect (pymupdf.Rect): The rectangle used by the helper. + fontname (str): The font name to use for measurement or rendering. + base_font_size (float): The initial font size to try when fitting text. + font_obj (pymupdf.Font | None, optional): The font object used for measurement. Defaults to None. + + Returns: + tuple[str | None, float | None]: The fitted token text and font size. + """ + if rect.width <= 0 or rect.height <= 0: + return None, None + + available_width = max(rect.width - (2 * PDF_TAG_RECT_INSET), 1.0) + start_size = min(base_font_size, max(rect.height - 1.0, 1.0)) + if start_size < 1.0: + return None, None + + def _measure(text: str, size: float) -> float: + """ + Measures the width of a candidate token at the given font size. + + Args: + text (str): The text value being normalized or searched. + size (float): The font size used for the current measurement. + + Returns: + float: The measured width of the candidate text. + """ + if font_obj is not None: + try: + return font_obj.text_length(text, fontsize=size) + except Exception: + pass + return pymupdf.get_text_length(text, fontname=fontname, fontsize=size) + + for size in _iter_font_sizes(start_size): + for candidate in _build_display_token_candidates(token): + if _measure(candidate, size) <= available_width + 0.1: + return candidate, size + + return None, None + + +_BASE14_FONT_CACHE: dict[str, pymupdf.Font] = {} + + +@lru_cache(maxsize=None) +def _cached_base14_font(name: str) -> pymupdf.Font: + """ + Loads and caches a Base-14 font by name. + + Args: + name (str): The Base-14 font name to load. + + Returns: + pymupdf.Font: The cached Base-14 font object. + """ + return pymupdf.Font(name) + + +def _get_base14_font(style: dict[str, Any]) -> pymupdf.Font: + """ + Returns the cached Base-14 font object for a style dictionary. + + Args: + style (dict[str, Any]): The style dictionary being analyzed. + + Returns: + pymupdf.Font: The cached Base-14 font for the style. + """ + name = _base14_fontname_for_style(style) + font = _BASE14_FONT_CACHE.get(name) + if font is None: + font = _cached_base14_font(name) + _BASE14_FONT_CACHE[name] = font + return font + + +def _rect_vertical_overlap(left: pymupdf.Rect, right: pymupdf.Rect) -> float: + """ + Calculates the vertical overlap ratio between two rectangles. + + Args: + left (pymupdf.Rect): The left rectangle or label to compare. + right (pymupdf.Rect): The right rectangle or label to compare. + + Returns: + float: The vertical overlap ratio between the rectangles. + """ + overlap = max(0.0, min(left.y1, right.y1) - max(left.y0, right.y0)) + min_height = max(min(left.height, right.height), 1e-6) + return overlap / min_height + + +def _group_adjacent_rects( + rects: list[pymupdf.Rect], max_gap: float +) -> list[pymupdf.Rect]: + """ + Merges horizontally adjacent rectangles that belong to the same segment. + + Args: + rects (list[pymupdf.Rect]): The `rects` value used by this helper. + max_gap (float): The `max_gap` value used by this helper. + + Returns: + list[pymupdf.Rect]: The merged rectangle groups. + """ + if not rects: + return [] + + ordered = sorted(rects, key=lambda rect: (rect.y0, rect.x0, rect.x1)) + groups: list[list[pymupdf.Rect]] = [[ordered[0]]] + + for rect in ordered[1:]: + previous = groups[-1][-1] + gap = rect.x0 - previous.x1 + if _rect_vertical_overlap(previous, rect) >= 0.5 and gap <= max_gap: + groups[-1].append(rect) + else: + groups.append([rect]) + + merged_rects: list[pymupdf.Rect] = [] + for group in groups: + merged = pymupdf.Rect(group[0]) + for rect in group[1:]: + merged.include_rect(rect) + merged_rects.append(merged) + + return merged_rects diff --git a/aymurai/text/anonymization/pdf/layout.py b/aymurai/text/anonymization/pdf/layout.py new file mode 100644 index 00000000..50ce529a --- /dev/null +++ b/aymurai/text/anonymization/pdf/layout.py @@ -0,0 +1,510 @@ +from __future__ import annotations + +import re +from copy import deepcopy +from typing import Any +from unicodedata import normalize + +import pymupdf +from jiwer import cer + +from aymurai.logger import get_logger +from aymurai.text.anonymization.alignment import ( + _label_replacement_end as _label_end, +) +from aymurai.text.anonymization.alignment import ( + _label_replacement_start as _label_start, +) +from aymurai.text.anonymization.alignment import ( + resolve_render_token, +) +from aymurai.text.anonymization.pdf.common import ( + PDF_TAG_RECT_GAP_FACTOR, + PDF_TAG_RECT_GAP_MAX, + PDF_TAG_RECT_GAP_MIN, + _build_flexible_pattern, + _build_spans_detail, + _font_size, + _group_adjacent_rects, + _line_style, + _line_text, + _rect_tuple, + _rect_vertical_overlap, +) + +logger = get_logger(__name__) + + +def _same_boundary_candidate(left: dict, right: dict) -> bool: + """ + Checks whether two labels can share a merged boundary token. + + Args: + left (dict): The left rectangle or label to compare. + right (dict): The right rectangle or label to compare. + + Returns: + bool: Whether the labels can share a boundary token. + """ + left_attrs = left.get("attrs") or {} + right_attrs = right.get("attrs") or {} + + if left_attrs.get("aymurai_label") != right_attrs.get("aymurai_label"): + return False + + left_cid = left_attrs.get("canonical_entity_id") + right_cid = right_attrs.get("canonical_entity_id") + if left_cid and right_cid and str(left_cid) != str(right_cid): + return False + + left_text = str(left.get("text") or "").strip() + right_text = str(right.get("text") or "").strip() + return bool(left_text and right_text) + + +def _resolve_token(label: dict, render_context: dict[str, Any] | None) -> str: + """ + Resolves the logical replacement token for a label. + + Args: + label (dict): The label metadata being processed. + render_context (dict[str, Any] | None): The rendering context used to resolve replacement tokens. + + Returns: + str: The logical token that should replace the label. + """ + boundary_token = label.get("_boundary_token") + if boundary_token: + return boundary_token + + token = resolve_render_token(label, render_context) + return token or "ENT" + + +def _apply_minimal_boundary_merge( + paragraphs: list[dict], + render_context: dict[str, Any] | None, +) -> None: + """ + Propagates a shared token across paragraph-boundary label pairs. + + Args: + paragraphs (list[dict]): The paragraph collection being processed. + render_context (dict[str, Any] | None): The rendering context used to resolve replacement tokens. + """ + for left_par, right_par in zip(paragraphs, paragraphs[1:]): + left_doc = left_par.get("document") or "" + right_doc = right_par.get("document") or "" + left_labels = left_par.get("labels") or [] + right_labels = right_par.get("labels") or [] + + if not left_doc or not right_doc or not left_labels or not right_labels: + continue + + left_candidates = [ + label + for label in left_labels + if _label_end(label) >= max(0, len(left_doc) - 2) + ] + right_candidates = [label for label in right_labels if _label_start(label) <= 2] + + if not left_candidates or not right_candidates: + continue + + for left_label in left_candidates: + for right_label in right_candidates: + if not _same_boundary_candidate(left_label, right_label): + continue + + shared_token = _resolve_token(left_label, render_context) + if not shared_token: + shared_token = _resolve_token(right_label, render_context) + if shared_token: + left_label["_boundary_token"] = shared_token + right_label["_boundary_token"] = shared_token + break + + +def _build_layout_paragraphs(parsed_doc: Any) -> list[dict]: + """ + Builds normalized paragraph metadata from the parsed PDF layout. + + Args: + parsed_doc (Any): The parsed PDF layout document. + + Returns: + list[dict]: The normalized layout paragraphs extracted from the parsed document. + """ + chunks = parsed_doc.to_text( + page_chunks=True, + header=True, + footer=True, + show_progress=False, + ) + + paragraphs: list[dict] = [] + layout_index = 0 + for page_idx, (page, chunk) in enumerate(zip(parsed_doc.pages, chunks)): + page_text = chunk.get("text") or "" + page_boxes = chunk.get("page_boxes") or [] + + for box_meta in page_boxes: + box_idx = int(box_meta["index"]) + if box_idx >= len(page.boxes): + continue + + start, stop = box_meta.get("pos", (0, 0)) + box_text = normalize("NFKC", page_text[start:stop]).strip() + if not box_text: + continue + + box = page.boxes[box_idx] + line_entries: list[dict] = [] + line_text_chunks: list[str] = [] + line_cursor = 0 + + for line_idx, line in enumerate(box.textlines or []): + text = normalize("NFKC", _line_text(line)).strip() + if not text: + continue + + if line_text_chunks: + line_text_chunks.append("\n") + line_cursor += 1 + + line_start = line_cursor + line_text_chunks.append(text) + line_cursor += len(text) + line_end = line_cursor + style = _line_style(line) + spans_detail, strip_offset = _build_spans_detail(line) + + line_entries.append( + { + "page_index": page_idx, + "box_index": box_idx, + "line_index": line_idx, + "bbox": _rect_tuple(line["bbox"]), + "font_size": _font_size(line, float(style.get("size") or 10.0)), + "start": line_start, + "end": line_end, + "text": text, + "style": style, + "spans_detail": spans_detail, + "strip_offset": strip_offset, + } + ) + + line_text = "".join(line_text_chunks) + if not line_text: + continue + + paragraphs.append( + { + "plain_text": box_text, + "metadata": { + "layout_index": layout_index, + "page_index": page_idx, + "page_number": page.page_number, + "box_index": box_idx, + "boxclass": box.boxclass, + "box_bbox": ( + float(box.x0), + float(box.y0), + float(box.x1), + float(box.y1), + ), + "line_text": line_text, + "lines": line_entries, + }, + } + ) + layout_index += 1 + + return paragraphs + + +def _match_predictions_to_layout( + layout_paragraphs: list[dict], + preds: list[dict], +) -> list[dict]: + """ + Matches model predictions to the closest layout paragraphs. + + Args: + layout_paragraphs (list[dict]): The `layout_paragraphs` value used by this helper. + preds (list[dict]): The predictions to apply to the document. + + Returns: + list[dict]: The predictions annotated with their matched layout metadata. + """ + if not layout_paragraphs or not preds: + return [] + + available_indices = list(range(len(layout_paragraphs))) + all_indices = list(range(len(layout_paragraphs))) + matched: list[dict] = [] + + normalized_layout_texts = [ + normalize("NFKC", paragraph["plain_text"]).strip() + for paragraph in layout_paragraphs + ] + + for pred_idx, pred in enumerate(preds): + pred_text = normalize("NFKC", str(pred.get("document") or "")).strip() + if not pred_text: + continue + + candidate_pool = available_indices if available_indices else all_indices + exact_idx = next( + ( + idx + for idx in candidate_pool + if normalized_layout_texts[idx] == pred_text + ), + None, + ) + + if exact_idx is None: + exact_idx = min( + candidate_pool, + key=lambda idx: cer(pred_text, normalized_layout_texts[idx]), + ) + + paragraph = deepcopy(layout_paragraphs[exact_idx]) + paragraph["document"] = pred.get("document") or "" + paragraph["labels"] = pred.get("labels") or [] + paragraph["pred_index"] = pred_idx + matched.append(paragraph) + + if exact_idx in available_indices: + available_indices.remove(exact_idx) + + matched.sort(key=lambda paragraph: paragraph["metadata"]["layout_index"]) + return matched + + +def _pick_rect_group_for_segment( + page: pymupdf.Page, + line: dict, + text: str, + line_x_cursor: dict[tuple[int, int, int], float], +) -> pymupdf.Rect: + """ + Chooses the best rectangle group for a text segment on the page. + + Args: + page (pymupdf.Page): The PDF page being processed. + line (dict): The parsed line metadata being processed. + text (str): The text value being normalized or searched. + line_x_cursor (dict[tuple[int, int, int], float]): The per-line cursor used to keep page searches stable. + + Returns: + pymupdf.Rect | None: The chosen rectangle group for the segment, if found. + """ + clip = pymupdf.Rect(line["bbox"]) + rects = [rect for rect in page.search_for(text, clip=clip) if rect.intersects(clip)] + if not rects: + return clip + + max_gap = min( + max(clip.height * PDF_TAG_RECT_GAP_FACTOR, PDF_TAG_RECT_GAP_MIN), + PDF_TAG_RECT_GAP_MAX, + ) + grouped_rects = _group_adjacent_rects(rects, max_gap=max_gap) + + line_key = (line["page_index"], line["box_index"], line["line_index"]) + min_x = line_x_cursor.get(line_key, clip.x0 - 1) + + for rect in grouped_rects: + if rect.x0 >= min_x - 0.5: + line_x_cursor[line_key] = rect.x1 + return rect + + chosen = grouped_rects[0] + line_x_cursor[line_key] = chosen.x1 + return chosen + + +def _normalize_line_chars(spans: list[dict]) -> list[dict[str, Any]]: + """ + Normalizes per-character span data into searchable character entries. + + Args: + spans (list[dict]): The span collection to normalize into character entries. + + Returns: + list[dict[str, Any]]: The normalized character entries for the line. + """ + chars: list[dict[str, Any]] = [] + for span in spans: + for char in span.get("chars") or []: + norm_text = normalize("NFKC", str(char.get("c") or "")) + if not norm_text: + continue + bbox = pymupdf.Rect(char["bbox"]) + for norm_char in norm_text: + chars.append({"char": norm_char, "bbox": bbox}) + return chars + + +def _line_chars_from_page(page: pymupdf.Page, line: dict) -> list[dict[str, Any]]: + """ + Extracts character-level geometry for a parsed line from the page text. + + Args: + page (pymupdf.Page): The PDF page being processed. + line (dict): The parsed line metadata being processed. + + Returns: + list[dict[str, Any]]: The character entries extracted from the page. + """ + clip = pymupdf.Rect(line["bbox"]) + raw = page.get_text("rawdict", clip=clip) + target_text = normalize("NFKC", str(line.get("text") or "")).strip() + + best_chars: list[dict[str, Any]] = [] + best_score: tuple[float, float, float] | None = None + + for block in raw.get("blocks") or []: + if block.get("type", 0) != 0: + continue + for raw_line in block.get("lines") or []: + chars = _normalize_line_chars(raw_line.get("spans") or []) + if not chars: + continue + + candidate_rect = pymupdf.Rect(raw_line["bbox"]) + candidate_text = "".join(entry["char"] for entry in chars).strip() + overlap = ( + _rect_vertical_overlap(candidate_rect, clip) + if candidate_rect.intersects(clip) + else 0.0 + ) + text_score = 0.0 + if target_text or candidate_text: + text_score = ( + 0.0 + if target_text == candidate_text + else cer(target_text, candidate_text) + ) + bbox_score = ( + abs(candidate_rect.x0 - clip.x0) + + abs(candidate_rect.y0 - clip.y0) + + abs(candidate_rect.x1 - clip.x1) + + abs(candidate_rect.y1 - clip.y1) + ) / 100.0 + score = (1.0 - overlap, text_score, bbox_score) + if best_score is None or score < best_score: + best_score = score + best_chars = chars + + return best_chars + + +def _line_chars_text(chars: list[dict[str, Any]]) -> str: + """ + Builds the searchable text for a character entry list. + + Args: + chars (list[dict[str, Any]]): The character entry list being processed. + + Returns: + str: The concatenated character text. + """ + return "".join(str(entry.get("char") or "") for entry in chars) + + +def _find_line_char_span( + chars: list[dict[str, Any]], + text: str, + *, + start: int = 0, + raw_text: str | None = None, +) -> tuple[int, int] | None: + """ + Finds the character span for a text fragment inside a line. + + Args: + chars (list[dict[str, Any]]): The character entry list being processed. + text (str): The text value being normalized or searched. + start (int, optional): The preferred start offset for the search. Defaults to 0. + raw_text (str | None, optional): The raw line text used as a fallback search surface. Defaults to None. + + Returns: + tuple[int, int] | None: The start and end character offsets, if found. + """ + if not chars or not text: + return None + + haystack = raw_text if raw_text is not None else _line_chars_text(chars) + pattern = _build_flexible_pattern(text) + + def _search(offset: int) -> tuple[int, int] | None: + """ + Searches for the candidate span from the provided offset. + + Args: + offset (int): The search offset used by the nested helper. + + Returns: + tuple[int, int] | None: The matching span for the current offset, if found. + """ + exact_idx = haystack.find(text, offset) + flexible_span = None + if pattern: + match = re.search(pattern, haystack[offset:]) + if match is not None: + flexible_span = (offset + match.start(), offset + match.end()) + + if exact_idx < 0: + return flexible_span + exact_span = (exact_idx, exact_idx + len(text)) + if flexible_span is None: + return exact_span + return min(exact_span, flexible_span, key=lambda span: span[0]) + + span = _search(start) + if span is None and start > 0: + span = _search(0) + return span + + +def _rect_from_char_slice( + chars: list[dict[str, Any]], + start: int, + end: int, +) -> pymupdf.Rect | None: + """ + Builds a rectangle covering the requested character slice. + + Args: + chars (list[dict[str, Any]]): The character entry list being processed. + start (int): The preferred start offset for the search. + end (int): The `end` value used by this helper. + + Returns: + pymupdf.Rect | None: The rectangle covering the requested character slice. + """ + if not chars: + return None + + slice_start = max(int(start), 0) + slice_end = min(int(end), len(chars)) + if slice_end <= slice_start: + return None + + segment = chars[slice_start:slice_end] + if not segment: + return None + + boxes = [entry["bbox"] for entry in segment if str(entry["char"]).strip()] + if not boxes: + boxes = [entry["bbox"] for entry in segment] + if not boxes: + return None + + rect = pymupdf.Rect(boxes[0]) + for bbox in boxes[1:]: + rect.include_rect(bbox) + return rect diff --git a/aymurai/text/anonymization/pdf/ops.py b/aymurai/text/anonymization/pdf/ops.py new file mode 100644 index 00000000..fb5a324f --- /dev/null +++ b/aymurai/text/anonymization/pdf/ops.py @@ -0,0 +1,967 @@ +from __future__ import annotations + +from typing import Any + +import pymupdf + +from aymurai.logger import get_logger +from aymurai.text.anonymization.alignment import ( + _label_replacement_start as _label_start, +) +from aymurai.text.anonymization.alignment import ( + _label_replacement_text as _label_surface_text, +) +from aymurai.text.anonymization.pdf.common import ( + PDF_TAG_RECT_GAP_MAX, + PDF_TAG_RECT_INSET, + PDF_TAG_RECT_X_PADDING, + PDF_TAG_RECT_Y_PADDING, + _base14_fontname_for_style, + _default_style, + _entity_style_from_spans, + _find_flexible, + _fit_display_token, + _get_base14_font, + _group_adjacent_rects, + _rect_vertical_overlap, +) +from aymurai.text.anonymization.pdf.layout import ( + _find_line_char_span, + _line_chars_from_page, + _line_chars_text, + _pick_rect_group_for_segment, + _rect_from_char_slice, + _resolve_token, +) +from aymurai.text.anonymization.pdf.widgets import ( + _apply_widget_ops, + _entity_overlaps_widget, + _page_widget_infos, + _prepare_signature_widget_ops, +) + +logger = get_logger(__name__) + +_IMAGE_OVERLAP_THRESHOLD = 0.3 + + +def _padded_rect(rect: pymupdf.Rect, clip: pymupdf.Rect) -> pymupdf.Rect: + """ + Pads a rectangle within the provided clipping bounds. + + Args: + rect (pymupdf.Rect): The rectangle used by the helper. + clip (pymupdf.Rect): The clipping rectangle to constrain the operation. + + Returns: + pymupdf.Rect: The padded rectangle clipped to the provided bounds. + """ + padded = pymupdf.Rect(rect) + padded.x0 = max(clip.x0, padded.x0 - PDF_TAG_RECT_X_PADDING) + padded.y0 = max(clip.y0, padded.y0 - PDF_TAG_RECT_Y_PADDING) + padded.x1 = min(clip.x1, padded.x1 + PDF_TAG_RECT_X_PADDING) + padded.y1 = min(clip.y1, padded.y1 + PDF_TAG_RECT_Y_PADDING) + return padded + + +def _render_rect(rect: pymupdf.Rect) -> pymupdf.Rect: + """ + Builds the token rendering rectangle from the padded canvas rectangle. + + Args: + rect (pymupdf.Rect): The rectangle used by the helper. + + Returns: + pymupdf.Rect: The rectangle used to render the replacement token. + """ + render_rect = pymupdf.Rect(rect) + inset = min(PDF_TAG_RECT_INSET, max(render_rect.height * 0.1, 0.0)) + render_rect.x0 += inset + render_rect.x1 -= inset + if render_rect.x1 <= render_rect.x0: + render_rect = pymupdf.Rect(rect) + return render_rect + + +def _text_redact_rect(rect: pymupdf.Rect) -> pymupdf.Rect: + """ + Builds the redaction rectangle used to remove original text. + + Args: + rect (pymupdf.Rect): The rectangle used by the helper. + + Returns: + pymupdf.Rect: The rectangle used for text redaction. + """ + redact_rect = pymupdf.Rect(rect) + edge_inset = min(0.25, max(redact_rect.width * 0.01, 0.05)) + if redact_rect.width > (2 * edge_inset): + redact_rect.x0 += edge_inset + redact_rect.x1 -= edge_inset + return redact_rect + + +def _build_page_op( + rect: pymupdf.Rect, + line: dict | None, + token: str, + is_image: bool = False, + entity_style: dict[str, Any] | None = None, +) -> dict[str, Any]: + """ + Builds the rendering operation metadata for a matched page segment. + + Args: + rect (pymupdf.Rect): The rectangle used by the helper. + line (dict | None): The parsed line metadata being processed. + token (str): The logical replacement token being processed. + is_image (bool, optional): Whether the operation is intended for image-backed content. Defaults to False. + entity_style (dict[str, Any] | None, optional): The resolved style dictionary for the entity text. Defaults to None. + + Returns: + dict[str, Any]: The rendering operation metadata for the segment. + """ + line_clip = pymupdf.Rect(line["bbox"]) if line else pymupdf.Rect(rect) + canvas_rect = _padded_rect(rect, line_clip) + render_rect = _render_rect(canvas_rect) + style = entity_style or (line or {}).get("style") or _default_style() + base_font_size = float((line or {}).get("font_size") or style.get("size") or 10.0) + + # Always use Base-14 fonts: they carry correct bold/italic weight and + # contain all glyphs needed for tags (<, >, _, digits, letters). + # Subset font buffers extracted from the PDF lack many of these glyphs. + fontname = _base14_fontname_for_style(style) + font_obj = _get_base14_font(style) + + display_token, fitted_size = _fit_display_token( + token, + render_rect, + fontname, + base_font_size, + font_obj=font_obj, + ) + + if not display_token or fitted_size is None: + logger.warning( + "Could not fit PDF token '%s' inside rect=%s", + token, + tuple(round(value, 2) for value in canvas_rect), + ) + + return { + "redact_rect": _text_redact_rect(rect), + "background_rect": canvas_rect, + "canvas_rect": canvas_rect, + "render_rect": render_rect, + "line_rect": line_clip, + "text": display_token, + "logical_token": token, + "fontname": fontname, + "fontsize": fitted_size, + "text_align": pymupdf.TEXT_ALIGN_LEFT, + "text_color": style.get("color") or (0.0, 0.0, 0.0), + "style": style, + } + + +def _image_rects_for_clip( + page: pymupdf.Page, + clip: pymupdf.Rect, +) -> list[pymupdf.Rect]: + """ + Collects image rectangles that overlap the given page region. + + Args: + page (pymupdf.Page): The PDF page being processed. + clip (pymupdf.Rect): The clipping rectangle to constrain the operation. + + Returns: + list[pymupdf.Rect]: The image rectangles that overlap the clip region. + """ + rects: list[pymupdf.Rect] = [] + for img_info in page.get_image_info(): + bbox = img_info.get("bbox") + if bbox is None: + continue + img_rect = pymupdf.Rect(bbox) + if img_rect.intersects(clip) and img_rect.get_area() > 0: + rects.append(img_rect) + return rects + + +def _squared_distance_between_rect_centers( + left: pymupdf.Rect, + right: pymupdf.Rect, +) -> float: + """ + Computes the squared distance between two rectangle centers. + + Args: + left (pymupdf.Rect): The first rectangle. + right (pymupdf.Rect): The second rectangle. + + Returns: + float: The squared distance between rectangle centers. + """ + left_center = ((left.x0 + left.x1) / 2.0, (left.y0 + left.y1) / 2.0) + right_center = ((right.x0 + right.x1) / 2.0, (right.y0 + right.y1) / 2.0) + return (left_center[0] - right_center[0]) ** 2 + ( + left_center[1] - right_center[1] + ) ** 2 + + +def _refine_signature_text_rect( + page: pymupdf.Page, + entity_text: str, + widget_rect: pymupdf.Rect, + current_rect: pymupdf.Rect, +) -> pymupdf.Rect: + """ + Finds a tighter text rectangle for signer names inside signature widgets. + + Args: + page (pymupdf.Page): The PDF page being processed. + entity_text (str): The entity text being mapped. + widget_rect (pymupdf.Rect): The signature widget rectangle. + current_rect (pymupdf.Rect): The currently resolved entity rectangle. + + Returns: + pymupdf.Rect: The refined rectangle when available, otherwise current_rect. + """ + widget_clip = pymupdf.Rect(widget_rect) + hits = [ + pymupdf.Rect(hit) + for hit in page.search_for(entity_text, clip=widget_clip) + if pymupdf.Rect(hit).intersects(widget_clip) + ] + if not hits: + return pymupdf.Rect(current_rect) + + target = pymupdf.Rect(current_rect) + intersecting_hits = [hit for hit in hits if hit.intersects(target)] + candidates = intersecting_hits or hits + return pymupdf.Rect( + min( + candidates, + key=lambda hit: _squared_distance_between_rect_centers(hit, target), + ) + ) + + +def _build_signature_page_op( + page: pymupdf.Page, + entity_text: str, + widget_info: dict[str, Any], + current_rect: pymupdf.Rect, + token: str, + entity_style: dict[str, Any] | None = None, +) -> dict[str, Any]: + """ + Builds a signature-specific operation scoped to the sensitive text only. + + Args: + page (pymupdf.Page): The PDF page being processed. + entity_text (str): The sensitive text being replaced. + widget_info (dict[str, Any]): The signature widget metadata. + current_rect (pymupdf.Rect): The initially resolved text rectangle. + token (str): The logical replacement token. + entity_style (dict[str, Any] | None): The text style to render with. + + Returns: + dict[str, Any]: The signature replacement operation. + """ + refined_rect = _refine_signature_text_rect( + page, + entity_text, + widget_info["rect"], + current_rect, + ) + op = _build_page_op( + refined_rect, + None, + token, + entity_style=entity_style or widget_info.get("style") or None, + ) + op["widget_xref"] = widget_info["xref"] + op["widget_rect"] = widget_info["rect"] + return op + + +def _entity_overlaps_image( + page: pymupdf.Page, + entity_rect: pymupdf.Rect, + image_rects: list[pymupdf.Rect], +) -> pymupdf.Rect | None: + """ + Checks whether an entity rectangle overlaps a detected image. + + Args: + page (pymupdf.Page): The PDF page being processed. + entity_rect (pymupdf.Rect): The rectangle representing the entity on the page. + image_rects (list[pymupdf.Rect]): The image rectangles available for overlap checks. + + Returns: + pymupdf.Rect | None: The overlapping image rectangle, if one exists. + """ + for img_rect in image_rects: + overlap = _rect_vertical_overlap(entity_rect, img_rect) + if overlap >= _IMAGE_OVERLAP_THRESHOLD and entity_rect.intersects(img_rect): + return img_rect + return None + + +def _collect_page_redactions( + doc: pymupdf.Document, + paragraphs: list[dict], + render_context: dict[str, Any] | None, +) -> dict[int, list[dict]]: + """ + Collects text, widget, and signature redaction operations for a document. + + Args: + doc (pymupdf.Document): The PDF document being processed. + paragraphs (list[dict]): The paragraph collection being processed. + render_context (dict[str, Any] | None): The rendering context used to resolve replacement tokens. + + Returns: + tuple[dict[int, list[dict]], dict[int, list[dict]], dict[int, list[dict]]]: The page, text-widget, and signature-widget operations. + """ + page_ops: dict[int, list[dict]] = {} + widget_ops: dict[int, list[dict]] = {} + signature_widget_ops: dict[int, list[dict]] = {} + line_x_cursor: dict[tuple[int, int, int], float] = {} + line_char_cache: dict[tuple[int, int, int], list[dict[str, Any]]] = {} + line_char_text_cache: dict[tuple[int, int, int], str] = {} + line_char_cursor: dict[tuple[int, int, int], int] = {} + + # Pre-compute image rects and widgets per page + page_image_rects: dict[int, list[pymupdf.Rect]] = {} + page_widgets: dict[int, list[dict[str, Any]]] = {} + + for paragraph in paragraphs: + metadata = paragraph.get("metadata") or {} + lines = metadata.get("lines") or [] + if not lines: + continue + + page_index = int(metadata["page_index"]) + page = doc[page_index] + line_text = metadata.get("line_text") or "" + box_clip = pymupdf.Rect(metadata.get("box_bbox") or page.rect) + document = paragraph.get("document") or "" + labels = sorted(paragraph.get("labels") or [], key=_label_start) + search_cursor = 0 + + # Lazy-load image rects and widget infos for this page + if page_index not in page_image_rects: + page_image_rects[page_index] = _image_rects_for_clip(page, page.rect) + if page_index not in page_widgets: + page_widgets[page_index] = _page_widget_infos(page) + + for label in labels: + entity_text = _label_surface_text(label, document).strip() + if not entity_text: + continue + + token = _resolve_token(label, render_context) + + span = _find_flexible(line_text, entity_text, start=search_cursor) + if span is None: + span = _find_flexible(line_text, entity_text, start=0) + if span is None: + # -- Fallback: direct page search -- + fallback_rects = [ + rect + for rect in page.search_for(entity_text, clip=box_clip) + if rect.intersects(box_clip) + ] + + # Check if this is a widget-backed entity before falling back to images + if fallback_rects: + fallback_widget = _entity_overlaps_widget( + fallback_rects[0], + page_widgets[page_index], + ) + if fallback_widget is not None: + if ( + fallback_widget["field_type"] + == pymupdf.PDF_WIDGET_TYPE_TEXT + ): + widget_ops.setdefault(page_index, []).append( + { + "widget_xref": fallback_widget["xref"], + "field_name": fallback_widget["field_name"], + "widget_info": fallback_widget, + "entity_text": entity_text, + "logical_token": token, + } + ) + continue + if ( + fallback_widget["field_type"] + == pymupdf.PDF_WIDGET_TYPE_SIGNATURE + ): + op = _build_signature_page_op( + page, + entity_text, + fallback_widget, + fallback_rects[0], + token, + entity_style=fallback_widget.get("style") or None, + ) + signature_widget_ops.setdefault(page_index, []).append(op) + continue + + # Check if this is an image-based entity + if not fallback_rects: + img_match = _try_image_entity( + page, + entity_text, + box_clip, + page_image_rects[page_index], + ) + if img_match is not None: + op = _build_page_op( + img_match, + lines[0] if lines else None, + token, + is_image=True, + ) + op["image_rect"] = img_match + page_ops.setdefault(page_index, []).append(op) + continue + + if fallback_rects: + grouped_rects = _group_adjacent_rects( + fallback_rects, max_gap=PDF_TAG_RECT_GAP_MAX + ) + fallback_line = lines[0] if lines else None + + # Check if any of these rects overlap an image + for rect in grouped_rects: + img_rect = _entity_overlaps_image( + page, + rect, + page_image_rects[page_index], + ) + op = _build_page_op( + rect, + fallback_line, + token, + is_image=(img_rect is not None), + ) + if img_rect is not None: + op["image_rect"] = img_rect + page_ops.setdefault(page_index, []).append(op) + continue + + logger.warning( + "Could not map label '%s' on page=%s box=%s", + entity_text, + metadata.get("page_number"), + metadata.get("box_index"), + ) + continue + + search_cursor = span[1] + + # Collect line segments this entity spans + segments: list[ + tuple[ + dict, + str, + pymupdf.Rect, + pymupdf.Rect | None, + dict, + dict[str, Any] | None, + ] + ] = [] + for line in lines: + overlap_start = max(span[0], line["start"]) + overlap_end = min(span[1], line["end"]) + if overlap_end <= overlap_start: + continue + + segment_text = line_text[overlap_start:overlap_end].strip() + if not segment_text: + continue + + line_key = ( + line["page_index"], + line["box_index"], + line["line_index"], + ) + line_chars = line_char_cache.get(line_key) + if line_chars is None: + line_chars = _line_chars_from_page(page, line) + line_char_cache[line_key] = line_chars + + line_char_text = line_char_text_cache.get(line_key) + if line_char_text is None: + line_char_text = _line_chars_text(line_chars) + line_char_text_cache[line_key] = line_char_text + + raw_span = _find_line_char_span( + line_chars, + segment_text, + start=line_char_cursor.get(line_key, 0), + raw_text=line_char_text, + ) + rect = None + if raw_span is not None: + line_char_cursor[line_key] = raw_span[1] + rect = _rect_from_char_slice(line_chars, raw_span[0], raw_span[1]) + + if rect is None: + raw_start = ( + overlap_start - line["start"] + int(line.get("strip_offset", 0)) + ) + raw_end = ( + overlap_end - line["start"] + int(line.get("strip_offset", 0)) + ) + rect = _rect_from_char_slice(line_chars, raw_start, raw_end) + if rect is None: + rect = _pick_rect_group_for_segment( + page, + line, + segment_text, + line_x_cursor, + ) + + widget_info = _entity_overlaps_widget( + rect, + page_widgets[page_index], + ) + + # Check for image overlap + img_rect = _entity_overlaps_image( + page, + rect, + page_image_rects[page_index], + ) + + # Determine entity-specific style from the span that + # actually contains this text (not the line's dominant style) + offset_in_line = overlap_start - line["start"] + ent_style = _entity_style_from_spans(line, offset_in_line) + + segments.append( + (line, segment_text, rect, img_rect, ent_style, widget_info) + ) + + if not segments: + continue + + if len(segments) == 1: + # Single-line entity: route widget-backed content through the widget path. + line, _seg_text, rect, img_rect, ent_style, widget_info = segments[0] + if widget_info is not None: + if widget_info["field_type"] == pymupdf.PDF_WIDGET_TYPE_TEXT: + widget_ops.setdefault(page_index, []).append( + { + "widget_xref": widget_info["xref"], + "field_name": widget_info["field_name"], + "widget_info": widget_info, + "entity_text": entity_text, + "logical_token": token, + } + ) + continue + if widget_info["field_type"] == pymupdf.PDF_WIDGET_TYPE_SIGNATURE: + op = _build_signature_page_op( + page, + entity_text, + widget_info, + rect, + token, + entity_style=ent_style, + ) + signature_widget_ops.setdefault(page_index, []).append(op) + continue + + op = _build_page_op( + rect, + line, + token, + is_image=(img_rect is not None), + entity_style=ent_style, + ) + if img_rect is not None: + op["image_rect"] = img_rect + page_ops.setdefault(page_index, []).append(op) + else: + # Multi-line entity: write the token on the widest segment only; blank the others. + widest_idx = max( + range(len(segments)), + key=lambda i: segments[i][2].width, + ) + any_image = any(seg[3] is not None for seg in segments) + shared_image_rect = next( + (seg[3] for seg in segments if seg[3] is not None), + None, + ) + + signature_widget = None + if all(seg[5] is not None for seg in segments): + widget_xrefs = {int(seg[5]["xref"]) for seg in segments} + widget_types = {int(seg[5]["field_type"]) for seg in segments} + if len(widget_xrefs) == 1 and widget_types == { + pymupdf.PDF_WIDGET_TYPE_SIGNATURE + }: + signature_widget = segments[0][5] + + for seg_idx, ( + seg_line, + seg_text, + seg_rect, + seg_img, + seg_style, + seg_widget, + ) in enumerate(segments): + if signature_widget is not None: + op = _build_signature_page_op( + page, + seg_text, + signature_widget, + seg_rect, + token, + entity_style=seg_style, + ) + if seg_idx != widest_idx: + op["text"] = None + op["fontsize"] = None + signature_widget_ops.setdefault(page_index, []).append(op) + continue + + if seg_idx == widest_idx: + op = _build_page_op( + seg_rect, + seg_line, + token, + is_image=any_image, + entity_style=seg_style, + ) + if shared_image_rect is not None: + op["image_rect"] = shared_image_rect + else: + op = _build_page_op( + seg_rect, + seg_line, + token, + is_image=(seg_img is not None), + entity_style=seg_style, + ) + op["text"] = None + op["fontsize"] = None + if seg_img is not None: + op["image_rect"] = seg_img + + page_ops.setdefault(page_index, []).append(op) + + return page_ops, widget_ops, signature_widget_ops + + +def _try_image_entity( + page: pymupdf.Page, + entity_text: str, + clip: pymupdf.Rect, + image_rects: list[pymupdf.Rect], +) -> pymupdf.Rect | None: + """ + Finds the best image rectangle for an entity when text search fails. + + Args: + page (pymupdf.Page): The PDF page being processed. + entity_text (str): The entity text being mapped. + clip (pymupdf.Rect): The clipping rectangle to constrain the operation. + image_rects (list[pymupdf.Rect]): The image rectangles available for overlap checks. + + Returns: + pymupdf.Rect | None: The best image rectangle for the entity, if found. + """ + if not image_rects: + return None + + # Try unclipped text search — the entity might be rendered as real text + # on top of (or near) an image. + text_hits = page.search_for(entity_text) + if text_hits: + for hit_rect in text_hits: + for img_rect in image_rects: + if hit_rect.intersects(img_rect): + return img_rect + + # Fallback: pick the image whose intersection with *clip* is largest + best: pymupdf.Rect | None = None + best_area = 0.0 + for img_rect in image_rects: + if not img_rect.intersects(clip) or img_rect.get_area() <= 0: + continue + intersection = img_rect & clip + area = intersection.get_area() + if area > best_area: + best_area = area + best = img_rect + + return best + + +def _render_text_op(page: pymupdf.Page, op: dict) -> None: + """ + Renders a single anonymization token back onto a page. + + Args: + page (pymupdf.Page): The PDF page being processed. + op (dict): The operation dictionary being processed. + """ + canvas = pymupdf.Rect(op.get("background_rect") or op["canvas_rect"]) + if not op.get("skip_background_fill"): + page.draw_rect( + canvas, + color=(1, 1, 1), + fill=(1, 1, 1), + width=0, + overlay=True, + ) + + if not op.get("text") or not op.get("fontsize"): + return + + render = op["render_rect"] + line_rect = pymupdf.Rect(op.get("line_rect") or render) + style = op.get("style") or {} + base14_name = _base14_fontname_for_style(style) + font_obj = _get_base14_font(style) + + fontsize = float(op["fontsize"]) + descender = float(style.get("descender") or -0.2) + baseline_y = line_rect.y1 + (descender * fontsize) + baseline_y = min( + max(baseline_y, line_rect.y0 + (fontsize * 0.65)), + line_rect.y1 - 0.1, + ) + + text_width = font_obj.text_length(op["text"], fontsize=fontsize) + x_start = render.x0 + max((render.width - text_width) / 2.0, 0.0) + + try: + page.insert_text( + (x_start, baseline_y), + op["text"], + fontname=base14_name, + fontsize=fontsize, + color=op["text_color"], + overlay=True, + ) + return + except Exception as exc: + logger.debug("insert_text failed for '%s': %s", op["text"], exc) + + try: + tw = pymupdf.TextWriter(page.rect, color=op["text_color"]) + tw.fill_textbox( + render, + op["text"], + font=font_obj, + fontsize=fontsize, + align=op.get("text_align", pymupdf.TEXT_ALIGN_CENTER), + ) + tw.write_text(page, overlay=True) + return + except Exception as exc: + logger.debug("TextWriter failed for '%s': %s", op["text"], exc) + + try: + page.insert_textbox( + render, + op["text"], + fontname=base14_name, + fontsize=fontsize, + color=op["text_color"], + align=op.get("text_align", pymupdf.TEXT_ALIGN_CENTER), + overlay=True, + ) + except Exception as exc: + logger.warning( + "All text insertion methods failed for '%s': %s", + op["text"], + exc, + ) + + +def _page_asset_rect(op: dict[str, Any]) -> pymupdf.Rect | None: + """ + Resolves the asset rectangle associated with a page operation. + + Args: + op (dict[str, Any]): The operation dictionary being processed. + + Returns: + pymupdf.Rect | None: The asset rectangle associated with the operation, if any. + """ + asset_rect = op.get("asset_rect") or op.get("image_rect") + if asset_rect is None: + return None + return pymupdf.Rect(asset_rect) + + +def _partition_page_ops( + page_ops: dict[int, list[dict]], +) -> tuple[dict[int, list[dict]], dict[int, list[dict]]]: + """ + Splits page operations into text-only and asset-backed groups. + + Args: + page_ops (dict[int, list[dict]]): The collected page operations grouped by page index. + + Returns: + tuple[dict[int, list[dict]], dict[int, list[dict]]]: The text-only and asset-backed operations. + """ + text_ops: dict[int, list[dict]] = {} + asset_ops: dict[int, list[dict]] = {} + + for page_idx, ops in page_ops.items(): + for op in ops: + if _page_asset_rect(op) is None: + text_ops.setdefault(page_idx, []).append(op) + else: + asset_ops.setdefault(page_idx, []).append(op) + + return text_ops, asset_ops + + +def _apply_text_redactions( + doc: pymupdf.Document, + text_page_ops: dict[int, list[dict]], +) -> None: + """ + Applies text-only redactions and re-renders replacement tokens. + + Args: + doc (pymupdf.Document): The PDF document being processed. + text_page_ops (dict[int, list[dict]]): The text-only page operations grouped by page index. + """ + for page_idx, ops in text_page_ops.items(): + if not ops: + continue + + page = doc[page_idx] + for op in ops: + page.add_redact_annot( + op["redact_rect"], + text=None, + fill=(1, 1, 1), + cross_out=False, + ) + + page.apply_redactions( + images=pymupdf.PDF_REDACT_IMAGE_NONE, + graphics=pymupdf.PDF_REDACT_LINE_ART_NONE, + text=pymupdf.PDF_REDACT_TEXT_REMOVE, + ) + + for op in ops: + _render_text_op(page, op) + + +def _apply_asset_redactions( + doc: pymupdf.Document, + asset_page_ops: dict[int, list[dict]], +) -> None: + """ + Applies asset-backed redactions and re-renders replacement tokens. + + Args: + doc (pymupdf.Document): The PDF document being processed. + asset_page_ops (dict[int, list[dict]]): The asset-backed page operations grouped by page index. + """ + for page_idx, ops in asset_page_ops.items(): + if not ops: + continue + + page = doc[page_idx] + graphics_mode = pymupdf.PDF_REDACT_LINE_ART_NONE + + for op in ops: + asset_rect = _page_asset_rect(op) + if asset_rect is None: + continue + + page.add_redact_annot( + asset_rect, + text=None, + fill=(1, 1, 1), + cross_out=False, + ) + graphics_mode = max( + graphics_mode, + int(op.get("graphics_mode") or pymupdf.PDF_REDACT_LINE_ART_NONE), + ) + + page.apply_redactions( + images=pymupdf.PDF_REDACT_IMAGE_REMOVE, + graphics=graphics_mode, + text=pymupdf.PDF_REDACT_TEXT_REMOVE, + ) + + for op in ops: + _render_text_op(page, op) + + +def _apply_signature_redactions( + doc: pymupdf.Document, + signature_widget_ops: dict[int, list[dict]], +) -> None: + """ + Applies signer-name redactions without removing the full signature appearance. + + Args: + doc (pymupdf.Document): The PDF document being processed. + signature_widget_ops (dict[int, list[dict]]): The signature operations grouped by page index. + """ + for page_idx, ops in signature_widget_ops.items(): + if not ops: + continue + + page = doc[page_idx] + for op in ops: + page.add_redact_annot( + op["redact_rect"], + text=None, + fill=(1, 1, 1), + cross_out=False, + ) + + page.apply_redactions( + images=pymupdf.PDF_REDACT_IMAGE_PIXELS, + graphics=pymupdf.PDF_REDACT_LINE_ART_NONE, + text=pymupdf.PDF_REDACT_TEXT_REMOVE, + ) + + for op in ops: + _render_text_op(page, op) + + +def _apply_redactions( + doc: pymupdf.Document, + page_ops: dict[int, list[dict]], + widget_ops: dict[int, list[dict]], + signature_widget_ops: dict[int, list[dict]], +) -> None: + """ + Applies all collected PDF redactions in the correct order. + + Args: + doc (pymupdf.Document): The PDF document being processed. + page_ops (dict[int, list[dict]]): The collected page operations grouped by page index. + widget_ops (dict[int, list[dict]]): The collected text widget operations grouped by page index. + signature_widget_ops (dict[int, list[dict]]): The collected signature widget operations grouped by page index. + """ + _apply_widget_ops(doc, widget_ops) + _prepare_signature_widget_ops(doc, signature_widget_ops) + + text_page_ops, asset_page_ops = _partition_page_ops(page_ops) + + _apply_text_redactions(doc, text_page_ops) + _apply_asset_redactions(doc, asset_page_ops) + _apply_signature_redactions(doc, signature_widget_ops) diff --git a/aymurai/text/anonymization/pdf/sanitize.py b/aymurai/text/anonymization/pdf/sanitize.py new file mode 100644 index 00000000..ab1bf344 --- /dev/null +++ b/aymurai/text/anonymization/pdf/sanitize.py @@ -0,0 +1,292 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import Any + +import pymupdf + +from aymurai.logger import get_logger +from aymurai.settings import settings + +logger = get_logger(__name__) + + +def _pdf_metadata_mod_date() -> str: + """ + Builds the PDF metadata modification timestamp in UTC. + + Returns: + str: The PDF-formatted UTC modification timestamp. + """ + timestamp = datetime.now(timezone.utc) + return timestamp.strftime("D:%Y%m%d%H%M%S+00'00'") + + +def _append_cleanup_rect( + cleanup_rects: dict[int, list[pymupdf.Rect]], + page_idx: int, + rect: pymupdf.Rect | tuple[float, float, float, float] | None, +) -> None: + """ + Appends a cleanup rectangle for later document sanitization. + + Args: + cleanup_rects (dict[int, list[pymupdf.Rect]]): The cleanup rectangles grouped by page index. + page_idx (int): The page index associated with the operation. + rect (pymupdf.Rect | tuple[float, float, float, float] | None): The rectangle used by the helper. + """ + if rect is None: + return + + cleanup_rect = pymupdf.Rect(rect) + if cleanup_rect.get_area() <= 0: + return + cleanup_rects.setdefault(page_idx, []).append(cleanup_rect) + + +def _cleanup_rect_for_page_op(op: dict[str, Any]) -> pymupdf.Rect | None: + """ + Builds the cleanup rectangle for a standard page operation. + + Args: + op (dict[str, Any]): The operation dictionary being processed. + + Returns: + pymupdf.Rect | None: The cleanup rectangle for the page operation, if available. + """ + if op.get("image_rect") is not None: + cleanup_rect = pymupdf.Rect(op["image_rect"]) + redact_rect = op.get("redact_rect") + if redact_rect is not None: + cleanup_rect.include_rect(pymupdf.Rect(redact_rect)) + return cleanup_rect + + cleanup_source = ( + op.get("redact_rect") or op.get("background_rect") or op.get("canvas_rect") + ) + if cleanup_source is None: + return None + return pymupdf.Rect(cleanup_source) + + +def _cleanup_rect_for_widget_op(op: dict[str, Any]) -> pymupdf.Rect | None: + """ + Builds the cleanup rectangle for a text widget operation. + + Args: + op (dict[str, Any]): The operation dictionary being processed. + + Returns: + pymupdf.Rect | None: The cleanup rectangle for the widget operation, if available. + """ + widget_info = op.get("widget_info") or {} + widget_rect = widget_info.get("rect") + if widget_rect is None: + return None + return pymupdf.Rect(widget_rect) + + +def _cleanup_rect_for_signature_widget_op(op: dict[str, Any]) -> pymupdf.Rect | None: + """ + Builds the cleanup rectangle for a signature widget operation. + + Args: + op (dict[str, Any]): The operation dictionary being processed. + + Returns: + pymupdf.Rect | None: The cleanup rectangle for the signature widget operation, if available. + """ + cleanup_source = ( + op.get("redact_rect") or op.get("background_rect") or op.get("canvas_rect") + ) + if cleanup_source is None: + return None + return pymupdf.Rect(cleanup_source) + + +def _collect_link_cleanup_rects( + page_ops: dict[int, list[dict]], + widget_ops: dict[int, list[dict]], + signature_widget_ops: dict[int, list[dict]], +) -> dict[int, list[pymupdf.Rect]]: + """ + Collects cleanup rectangles used to prune overlapping links. + + Args: + page_ops (dict[int, list[dict]]): The collected page operations grouped by page index. + widget_ops (dict[int, list[dict]]): The collected text widget operations grouped by page index. + signature_widget_ops (dict[int, list[dict]]): The collected signature widget operations grouped by page index. + + Returns: + dict[int, list[pymupdf.Rect]]: The cleanup rectangles grouped by page index. + """ + cleanup_rects: dict[int, list[pymupdf.Rect]] = {} + + for page_idx, ops in page_ops.items(): + for op in ops: + _append_cleanup_rect(cleanup_rects, page_idx, _cleanup_rect_for_page_op(op)) + + for page_idx, ops in widget_ops.items(): + for op in ops: + _append_cleanup_rect( + cleanup_rects, + page_idx, + _cleanup_rect_for_widget_op(op), + ) + + for page_idx, ops in signature_widget_ops.items(): + for op in ops: + _append_cleanup_rect( + cleanup_rects, + page_idx, + _cleanup_rect_for_signature_widget_op(op), + ) + + return cleanup_rects + + +def _remove_overlapping_page_links( + doc: pymupdf.Document, + cleanup_rects: dict[int, list[pymupdf.Rect]], +) -> None: + """ + Deletes page links that overlap anonymized regions. + + Args: + doc (pymupdf.Document): The PDF document being processed. + cleanup_rects (dict[int, list[pymupdf.Rect]]): The cleanup rectangles grouped by page index. + """ + for page_idx, page_rects in cleanup_rects.items(): + if not page_rects: + continue + + page = doc[page_idx] + for link in list(page.get_links()): + link_rect = link.get("from") + if link_rect is None: + continue + link_rect = pymupdf.Rect(link_rect) + if not any(link_rect.intersects(rect) for rect in page_rects): + continue + try: + page.delete_link(link) + except Exception as exc: + logger.warning( + "Failed to delete PDF link on page=%s rect=%s: %s", + page_idx, + tuple(round(value, 2) for value in link_rect), + exc, + ) + + +def _remove_remaining_annotations(doc: pymupdf.Document) -> None: + """ + Deletes residual page annotations after sanitization. + + Args: + doc (pymupdf.Document): The PDF document being processed. + """ + for page_idx, page in enumerate(doc): + for annot in list(page.annots() or []): + try: + page.delete_annot(annot) + except Exception as exc: + logger.warning( + "Failed to delete residual PDF annotation on page=%s: %s", + page_idx, + exc, + ) + + +def _clear_standard_metadata(doc: pymupdf.Document) -> None: + """ + Clears the standard PDF metadata fields on a document. + + Args: + doc (pymupdf.Document): The PDF document being processed. + """ + doc.set_metadata( + { + "title": "", + "author": "", + "subject": "", + "keywords": "", + "creator": "", + "producer": "", + "creationDate": "", + "modDate": "", + "trapped": "", + } + ) + + +def _apply_aymurai_metadata(doc: pymupdf.Document) -> None: + """ + Applies the configured AymurAI tooling metadata fields to the PDF document. + + Args: + doc (pymupdf.Document): The PDF document being processed. + """ + metadata = dict(doc.metadata or {}) + metadata.update( + { + "title": metadata.get("title") or "", + "author": "", + "subject": metadata.get("subject") or "", + "keywords": metadata.get("keywords") or "", + "creator": settings.ANONYMIZATION_METADATA_CREATOR, + "producer": settings.ANONYMIZATION_METADATA_PRODUCER, + "creationDate": metadata.get("creationDate") or "", + "modDate": _pdf_metadata_mod_date(), + "trapped": metadata.get("trapped") or "", + } + ) + doc.set_metadata(metadata) + + +def _sanitize_document( + doc: pymupdf.Document, + cleanup_rects: dict[int, list[pymupdf.Rect]], +) -> None: + """ + Sanitizes document-level PDF metadata, attachments, and annotations. + + Args: + doc (pymupdf.Document): The PDF document being processed. + cleanup_rects (dict[int, list[pymupdf.Rect]]): The cleanup rectangles grouped by page index. + """ + _remove_overlapping_page_links(doc, cleanup_rects) + doc.scrub( + metadata=True, + xml_metadata=True, + javascript=True, + attached_files=True, + embedded_files=True, + thumbnails=True, + reset_responses=True, + hidden_text=True, + clean_pages=True, + remove_links=False, + reset_fields=False, + redactions=False, + ) + _remove_remaining_annotations(doc) + _clear_standard_metadata(doc) + _apply_aymurai_metadata(doc) + + get_xml_metadata = getattr(doc, "get_xml_metadata", None) + del_xml_metadata = getattr(doc, "del_xml_metadata", None) + if callable(get_xml_metadata) and callable(del_xml_metadata): + try: + xml_metadata = get_xml_metadata() + except Exception as exc: + logger.warning("Failed to read PDF XML metadata after scrub: %s", exc) + else: + if xml_metadata: + try: + del_xml_metadata() + except Exception as exc: + logger.warning( + "Failed to delete residual PDF XML metadata: %s", + exc, + ) diff --git a/aymurai/text/anonymization/pdf/watermark.py b/aymurai/text/anonymization/pdf/watermark.py new file mode 100644 index 00000000..c15d9aef --- /dev/null +++ b/aymurai/text/anonymization/pdf/watermark.py @@ -0,0 +1,522 @@ +from __future__ import annotations + +import os +from functools import lru_cache +from pathlib import Path +from typing import Any + +import pymupdf + +from aymurai.logger import get_logger +from aymurai.settings import settings + +logger = get_logger(__name__) + +WATERMARK_PREFIX_TEXT = "Documento anonimizado por " +WATERMARK_LINK_TEXT = "AymurAI" +WATERMARK_TEXT = f"{WATERMARK_PREFIX_TEXT}{WATERMARK_LINK_TEXT}" +WATERMARK_URL = "https://www.aymurai.info/" +WATERMARK_FONT_SIZE = 10.0 +WATERMARK_MARGIN_X = 24.0 +WATERMARK_BASELINE_MARGIN = 12.0 +WATERMARK_TOP_BASELINE = 22.0 +WATERMARK_RECT_PADDING_X = 4.0 +WATERMARK_RECT_PADDING_Y = 4.0 +WATERMARK_COLLISION_PADDING = 12.0 +WATERMARK_TEXT_COLOR = tuple(channel / 255 for channel in (192, 192, 192)) +WATERMARK_LINK_COLOR = tuple(channel / 255 for channel in (115, 190, 250)) + + +def _candidate_font_paths() -> tuple[list[Path], list[Path]]: + """ + Builds the ordered list of candidate font paths for the PDF watermark. + + Returns: + tuple[list[Path], list[Path]]: The regular and bold watermark font candidates. + """ + override_regular = ( + os.getenv("PDF_WATERMARK_FONT_REGULAR") or settings.PDF_WATERMARK_FONT_REGULAR + ) + override_bold = ( + os.getenv("PDF_WATERMARK_FONT_BOLD") or settings.PDF_WATERMARK_FONT_BOLD + ) + + regular_candidates: list[Path] = [] + bold_candidates: list[Path] = [] + + if override_regular: + regular_candidates.append(Path(override_regular).expanduser()) + if override_bold: + bold_candidates.append(Path(override_bold).expanduser()) + + resource_roots: list[Path] = [] + resources_base = Path(settings.RESOURCES_BASEPATH) + if resources_base.is_absolute(): + resource_roots.append(resources_base) + else: + resource_roots.append((Path("/workspace") / resources_base).resolve()) + resource_roots.append(resources_base) + + font_roots: list[Path] = [] + for root in resource_roots: + font_roots.extend([root / "fonts", root / "fonts" / "archivo"]) + + for root in font_roots: + regular_candidates.extend( + [ + root / "Archivo-Regular.ttf", + root / "Archivo-Regular.otf", + root / "Archivo[wdth,wght].ttf", + root / "Archivo-VariableFont_wdth,wght.ttf", + ] + ) + bold_candidates.extend( + [ + root / "Archivo-Bold.ttf", + root / "Archivo-Bold.otf", + root / "Archivo-BoldItalic.ttf", + root / "Archivo-VariableFont_wdth,wght.ttf", + root / "Archivo[wdth,wght].ttf", + ] + ) + + system_roots = [ + Path("/usr/share/fonts/truetype/archivo"), + Path("/usr/share/fonts/opentype/archivo"), + Path("/usr/local/share/fonts/archivo"), + Path.home() / ".local/share/fonts", + Path.home() / ".local/share/fonts/archivo", + ] + for root in system_roots: + regular_candidates.extend( + [ + root / "Archivo-Regular.ttf", + root / "Archivo-Regular.otf", + root / "Archivo[wdth,wght].ttf", + root / "Archivo-VariableFont_wdth,wght.ttf", + ] + ) + bold_candidates.extend( + [ + root / "Archivo-Bold.ttf", + root / "Archivo-Bold.otf", + root / "Archivo-BoldItalic.ttf", + root / "Archivo-VariableFont_wdth,wght.ttf", + root / "Archivo[wdth,wght].ttf", + ] + ) + + return regular_candidates, bold_candidates + + +def _first_existing_path(paths: list[Path]) -> str | None: + """ + Returns the first existing file path from the provided candidates. + + Args: + paths (list[Path]): The candidate paths to inspect. + + Returns: + str | None: The first existing file path, if one is found. + """ + seen: set[str] = set() + for path in paths: + expanded = path.expanduser() + resolved = str(expanded) + if resolved in seen: + continue + seen.add(resolved) + if expanded.exists() and expanded.is_file(): + return str(expanded) + return None + + +@lru_cache(maxsize=1) +def _watermark_font_paths() -> tuple[str | None, str | None]: + """ + Resolves the font paths used by the PDF watermark. + + Returns: + tuple[str | None, str | None]: The resolved regular and bold watermark font paths. + """ + regular_candidates, bold_candidates = _candidate_font_paths() + regular_path = _first_existing_path(regular_candidates) + bold_path = _first_existing_path(bold_candidates) + if regular_path is None and bold_path is not None: + regular_path = bold_path + if bold_path is None: + bold_path = regular_path + return regular_path, bold_path + + +@lru_cache(maxsize=1) +def _watermark_font_config() -> dict[str, Any]: + """ + Builds the font configuration used to render the PDF watermark. + + Returns: + dict[str, Any]: The watermark font configuration dictionary. + """ + regular_path, bold_path = _watermark_font_paths() + if regular_path: + try: + return { + "text_fontname": "archivo-watermark", + "text_fontfile": regular_path, + "text_font": pymupdf.Font(fontfile=regular_path), + "link_fontname": "archivo-watermark-bold", + "link_fontfile": bold_path or regular_path, + "link_font": pymupdf.Font(fontfile=bold_path or regular_path), + } + except Exception as exc: + logger.warning( + "Could not load Archivo font for PDF watermark, falling back to Base-14 fonts: %s", + exc, + ) + + return { + "text_fontname": "Helvetica", + "text_fontfile": None, + "text_font": pymupdf.Font("Helvetica"), + "link_fontname": "Helvetica-Bold", + "link_fontfile": None, + "link_font": pymupdf.Font("Helvetica-Bold"), + } + + +def _watermark_text_length( + text: str, + *, + font_obj: pymupdf.Font, + fontname: str, + fontsize: float, +) -> float: + """ + Measures the rendered width of watermark text. + + Args: + text (str): The text value being normalized or searched. + font_obj (pymupdf.Font): The font object used for measurement. + fontname (str): The font name to use for measurement or rendering. + fontsize (float): The font size used for measurement or rendering. + + Returns: + float: The rendered width of the watermark text. + """ + try: + return float(font_obj.text_length(text, fontsize=fontsize)) + except Exception: + return float( + pymupdf.get_text_length(text, fontname=fontname, fontsize=fontsize) + ) + + +def _insert_watermark_text( + page: pymupdf.Page, + point: tuple[float, float], + text: str, + *, + fontname: str, + fontsize: float, + color: tuple[float, float, float], + fontfile: str | None = None, +) -> None: + """ + Inserts watermark text onto a page using the resolved font settings. + + Args: + page (pymupdf.Page): The PDF page being processed. + point (tuple[float, float]): The insertion point on the page. + text (str): The text value being normalized or searched. + fontname (str): The font name to use for measurement or rendering. + fontsize (float): The font size used for measurement or rendering. + color (tuple[float, float, float]): The PDF RGB color used to render the text. + fontfile (str | None, optional): The optional font file path to embed for rendering. Defaults to None. + """ + kwargs: dict[str, Any] = { + "fontsize": fontsize, + "fontname": fontname, + "color": color, + "overlay": True, + } + if fontfile: + kwargs["fontfile"] = fontfile + page.insert_text(point, text, **kwargs) + + +def _expanded_rect(rect: pymupdf.Rect, padding: float) -> pymupdf.Rect: + """ + Expands a rectangle by a uniform padding in every direction. + + Args: + rect (pymupdf.Rect): The rectangle to expand. + padding (float): The amount of padding to apply on every side. + + Returns: + pymupdf.Rect: The expanded rectangle. + """ + return pymupdf.Rect( + rect.x0 - padding, + rect.y0 - padding, + rect.x1 + padding, + rect.y1 + padding, + ) + + +def _watermark_corner_order(page_index: int) -> list[str]: + """ + Builds the preferred watermark corner order for a page. + + Args: + page_index (int): The page index being processed. + + Returns: + list[str]: The ordered watermark corner candidates for the page. + """ + if page_index % 2 == 0: + return ["bottom-right", "bottom-left", "top-left", "top-right"] + return ["bottom-left", "top-left", "top-right", "bottom-right"] + + +def _watermark_layout_for_corner( + page: pymupdf.Page, + corner: str, + *, + prefix_width: float, + link_width: float, + total_width: float, +) -> dict[str, Any]: + """ + Builds the watermark geometry for a specific page corner. + + Args: + page (pymupdf.Page): The PDF page being processed. + corner (str): The corner identifier used to position the watermark. + prefix_width (float): The rendered width of the watermark prefix text. + link_width (float): The rendered width of the watermark link text. + total_width (float): The total rendered width of the watermark text. + + Returns: + dict[str, Any]: The watermark layout data for the corner. + """ + if corner.endswith("right"): + x_start = max( + WATERMARK_MARGIN_X, + page.rect.width - total_width - WATERMARK_MARGIN_X, + ) + else: + x_start = WATERMARK_MARGIN_X + + if corner.startswith("bottom"): + baseline_y = page.rect.height - WATERMARK_BASELINE_MARGIN + else: + baseline_y = WATERMARK_TOP_BASELINE + + link_x = x_start + prefix_width + text_top = baseline_y - WATERMARK_FONT_SIZE + banner_rect = pymupdf.Rect( + x_start - WATERMARK_RECT_PADDING_X, + text_top - WATERMARK_RECT_PADDING_Y, + x_start + total_width + WATERMARK_RECT_PADDING_X, + baseline_y + WATERMARK_RECT_PADDING_Y, + ) + link_rect = pymupdf.Rect( + link_x, + text_top, + link_x + link_width, + baseline_y + 2.0, + ) + + return { + "corner": corner, + "x_start": x_start, + "baseline_y": baseline_y, + "link_x": link_x, + "banner_rect": banner_rect, + "link_rect": link_rect, + } + + +def _occupied_page_rects(page: pymupdf.Page) -> list[pymupdf.Rect]: + """ + Collects page rectangles already occupied by visible content. + + Args: + page (pymupdf.Page): The PDF page being processed. + + Returns: + list[pymupdf.Rect]: The occupied rectangles found on the page. + """ + occupied: list[pymupdf.Rect] = [] + + text_data = page.get_text("dict") + for block in text_data.get("blocks", []): + bbox = block.get("bbox") + if bbox is None: + continue + rect = pymupdf.Rect(bbox) + if rect.get_area() <= 0: + continue + occupied.append(_expanded_rect(rect, WATERMARK_COLLISION_PADDING)) + + for drawing in page.get_drawings(): + rect = drawing.get("rect") + if rect is None: + continue + rect = pymupdf.Rect(rect) + if rect.get_area() <= 0: + continue + occupied.append(_expanded_rect(rect, WATERMARK_COLLISION_PADDING)) + + return occupied + + +def _watermark_overlap_score( + banner_rect: pymupdf.Rect, + occupied_rects: list[pymupdf.Rect], +) -> tuple[float, float, int]: + """ + Scores a watermark placement by the amount of page content it overlaps. + + Args: + banner_rect (pymupdf.Rect): The watermark banner rectangle being scored. + occupied_rects (list[pymupdf.Rect]): The occupied page rectangles used for overlap checks. + + Returns: + tuple[float, float, int]: The overlap ratio, overlap area, and overlap count for the placement. + """ + overlap_area = 0.0 + overlap_count = 0 + banner_area = max(banner_rect.get_area(), 1.0) + + for rect in occupied_rects: + if not banner_rect.intersects(rect): + continue + intersection = banner_rect & rect + area = intersection.get_area() + if area <= 0: + continue + overlap_area += area + overlap_count += 1 + + return overlap_area / banner_area, overlap_area, overlap_count + + +def _choose_watermark_layout( + page: pymupdf.Page, + page_index: int, + *, + prefix_width: float, + link_width: float, + total_width: float, +) -> dict[str, Any]: + """ + Selects the watermark placement with the least overlap on a page. + + Args: + page (pymupdf.Page): The PDF page being processed. + page_index (int): The page index being processed. + prefix_width (float): The rendered width of the watermark prefix text. + link_width (float): The rendered width of the watermark link text. + total_width (float): The total rendered width of the watermark text. + + Returns: + dict[str, Any]: The chosen watermark layout data. + """ + occupied_rects = _occupied_page_rects(page) + candidate_layouts = [ + _watermark_layout_for_corner( + page, + corner, + prefix_width=prefix_width, + link_width=link_width, + total_width=total_width, + ) + for corner in _watermark_corner_order(page_index) + ] + + best_layout = candidate_layouts[0] + best_score: tuple[float, float, int] | None = None + + for layout in candidate_layouts: + score = _watermark_overlap_score(layout["banner_rect"], occupied_rects) + if score[0] == 0.0 and score[1] == 0.0: + return layout + if best_score is None or score < best_score: + best_layout = layout + best_score = score + + return best_layout + + +def add_pdf_footer_watermark(doc: pymupdf.Document) -> None: + """ + Adds the anonymization watermark to the least crowded corner of each PDF page. + + Args: + doc (pymupdf.Document): The PDF document being processed. + """ + font_config = _watermark_font_config() + prefix_width = _watermark_text_length( + WATERMARK_PREFIX_TEXT, + font_obj=font_config["text_font"], + fontname=font_config["text_fontname"], + fontsize=WATERMARK_FONT_SIZE, + ) + link_width = _watermark_text_length( + WATERMARK_LINK_TEXT, + font_obj=font_config["link_font"], + fontname=font_config["link_fontname"], + fontsize=WATERMARK_FONT_SIZE, + ) + total_width = prefix_width + link_width + + for page_index, page in enumerate(doc): + layout = _choose_watermark_layout( + page, + page_index, + prefix_width=prefix_width, + link_width=link_width, + total_width=total_width, + ) + baseline_y = layout["baseline_y"] + x_start = layout["x_start"] + link_x = layout["link_x"] + + _insert_watermark_text( + page, + (x_start, baseline_y), + WATERMARK_PREFIX_TEXT, + fontname=font_config["text_fontname"], + fontsize=WATERMARK_FONT_SIZE, + color=WATERMARK_TEXT_COLOR, + fontfile=font_config["text_fontfile"], + ) + _insert_watermark_text( + page, + (link_x, baseline_y), + WATERMARK_LINK_TEXT, + fontname=font_config["link_fontname"], + fontsize=WATERMARK_FONT_SIZE, + color=WATERMARK_LINK_COLOR, + fontfile=font_config["link_fontfile"], + ) + + if layout["corner"].startswith("bottom"): + underline_y = min(page.rect.height - 1.0, baseline_y + 1.0) + else: + underline_y = baseline_y + 1.0 + page.draw_line( + (link_x, underline_y), + (link_x + link_width, underline_y), + color=WATERMARK_LINK_COLOR, + width=0.8, + overlay=True, + ) + page.insert_link( + { + "kind": pymupdf.LINK_URI, + "from": layout["link_rect"], + "uri": WATERMARK_URL, + } + ) diff --git a/aymurai/text/anonymization/pdf/widgets.py b/aymurai/text/anonymization/pdf/widgets.py new file mode 100644 index 00000000..266a912f --- /dev/null +++ b/aymurai/text/anonymization/pdf/widgets.py @@ -0,0 +1,332 @@ +from __future__ import annotations + +from typing import Any + +import pymupdf + +from aymurai.logger import get_logger +from aymurai.text.anonymization.pdf.common import ( + _build_display_token_candidates, + _default_style, + _find_flexible, + _get_base14_font, +) + +logger = get_logger(__name__) + + +def _signature_background_rect( + op: dict[str, Any], + widget_rect: pymupdf.Rect, +) -> pymupdf.Rect: + """ + Builds the background rectangle used for a signature replacement. + + Args: + op (dict[str, Any]): The operation dictionary being processed. + widget_rect (pymupdf.Rect): The rectangle occupied by the widget. + + Returns: + pymupdf.Rect: The background rectangle for the signature replacement. + """ + background_source = ( + op.get("canvas_rect") or op.get("redact_rect") or op.get("line_rect") + ) + background = pymupdf.Rect(background_source or widget_rect) + + redact_rect = op.get("redact_rect") + if redact_rect is not None: + background.include_rect(pymupdf.Rect(redact_rect)) + + # Keep the repaint area on the sensitive text line so the replacement + # background does not visually cover adjacent non-sensitive content + pad_x = max(background.height * 0.2, 0.5) + widget_clip = pymupdf.Rect(widget_rect) + + background.x0 = max(widget_clip.x0, background.x0 - pad_x) + background.y0 = max(widget_clip.y0, background.y0) + background.x1 = min(widget_clip.x1, background.x1 + pad_x) + background.y1 = min(widget_clip.y1, background.y1) + return background + + +def _widget_text_color(widget: pymupdf.Widget) -> tuple[float, float, float]: + """ + Extracts the text color configured on a PDF widget. + + Args: + widget (pymupdf.Widget): The widget being processed. + + Returns: + tuple[float, float, float]: The widget text color in PDF RGB components. + """ + values = list(widget.text_color or []) + if not values: + return (0.0, 0.0, 0.0) + if len(values) == 1: + shade = float(values[0]) + return (shade, shade, shade) + if len(values) >= 3: + return tuple(float(value) for value in values[:3]) + return (0.0, 0.0, 0.0) + + +def _style_from_widget(widget: pymupdf.Widget) -> dict[str, Any]: + """ + Builds a text style dictionary from a widget definition. + + Args: + widget (pymupdf.Widget): The widget being processed. + + Returns: + dict[str, Any]: The style dictionary derived from the widget. + """ + return { + "font": str(widget.text_font or ""), + "flags": 0, + "color": _widget_text_color(widget), + "size": float(widget.text_fontsize or 10.0), + "ascender": 0.8, + "descender": -0.2, + } + + +def _page_widget_infos(page: pymupdf.Page) -> list[dict[str, Any]]: + """ + Collects text and signature widget metadata for a page. + + Args: + page (pymupdf.Page): The PDF page being processed. + + Returns: + list[dict[str, Any]]: The widget metadata collected for the page. + """ + infos: list[dict[str, Any]] = [] + for widget in page.widgets() or []: + if widget.field_type not in ( + pymupdf.PDF_WIDGET_TYPE_TEXT, + pymupdf.PDF_WIDGET_TYPE_SIGNATURE, + ): + continue + infos.append( + { + "xref": int(widget.xref), + "field_type": int(widget.field_type), + "field_name": str(widget.field_name or ""), + "field_value": str(widget.field_value or ""), + "rect": pymupdf.Rect(widget.rect), + "style": _style_from_widget(widget), + } + ) + return infos + + +def _entity_overlaps_widget( + entity_rect: pymupdf.Rect, + widget_infos: list[dict[str, Any]], +) -> dict[str, Any] | None: + """ + Finds the widget that most overlaps the given entity rectangle. + + Args: + entity_rect (pymupdf.Rect): The rectangle representing the entity on the page. + widget_infos (list[dict[str, Any]]): The widget metadata available for overlap checks. + + Returns: + dict[str, Any] | None: The best overlapping widget info, if one exists. + """ + best_widget: dict[str, Any] | None = None + best_area = 0.0 + for widget_info in widget_infos: + widget_rect = widget_info["rect"] + if not entity_rect.intersects(widget_rect): + continue + area = (entity_rect & widget_rect).get_area() + if area > best_area: + best_area = area + best_widget = widget_info + return best_widget + + +def _fit_widget_token( + widget_info: dict[str, Any], + current_text: str, + entity_span: tuple[int, int], + token: str, +) -> str: + """ + Finds a token variant that fits inside a widget value. + + Args: + widget_info (dict[str, Any]): The widget metadata being processed. + current_text (str): The current widget text value. + entity_span (tuple[int, int]): The span of the entity inside the widget text. + token (str): The logical replacement token being processed. + + Returns: + str: The token variant that fits in the widget value. + """ + style = widget_info.get("style") or _default_style() + rect = pymupdf.Rect(widget_info["rect"]) + font_obj = _get_base14_font(style) + max_width = max(rect.width - 1.0, 1.0) + + prefix = current_text[: entity_span[0]] + suffix = current_text[entity_span[1] :] + + for candidate in _build_display_token_candidates(token): + candidate_text = f"{prefix}{candidate}{suffix}" + if ( + font_obj.text_length( + candidate_text, fontsize=float(style.get("size") or 10.0) + ) + <= max_width + 0.1 + ): + return candidate + + candidates = _build_display_token_candidates(token) + return candidates[0] if candidates else f"<{token}>" + + +def _apply_widget_ops( + doc: pymupdf.Document, + widget_ops: dict[int, list[dict]], +) -> None: + """ + Applies collected replacements to editable text widgets. + + Args: + doc (pymupdf.Document): The PDF document being processed. + widget_ops (dict[int, list[dict]]): The collected text widget operations grouped by page index. + """ + for page_idx, ops in widget_ops.items(): + if not ops: + continue + + page = doc[page_idx] + widgets = { + int(widget.xref): widget + for widget in (page.widgets() or []) + if widget.field_type == pymupdf.PDF_WIDGET_TYPE_TEXT + } + grouped: dict[int, list[dict]] = {} + for op in ops: + grouped.setdefault(int(op["widget_xref"]), []).append(op) + + for widget_xref, replacements in grouped.items(): + widget = widgets.get(widget_xref) + if widget is None: + logger.warning( + "Could not resolve PDF widget xref=%s on page=%s", + widget_xref, + page_idx, + ) + continue + + current_text = str(widget.field_value or "") + if not current_text: + continue + + search_cursor = 0 + changed = False + for replacement in replacements: + entity_text = replacement["entity_text"] + span = _find_flexible(current_text, entity_text, start=search_cursor) + if span is None: + span = _find_flexible(current_text, entity_text, start=0) + if span is None: + logger.warning( + "Could not map widget label '%s' in widget '%s' on page=%s", + entity_text, + replacement.get("field_name") or widget.field_name, + page_idx, + ) + continue + + token_text = _fit_widget_token( + replacement["widget_info"], + current_text, + span, + replacement["logical_token"], + ) + current_text = ( + f"{current_text[: span[0]]}{token_text}{current_text[span[1] :]}" + ) + search_cursor = span[0] + len(token_text) + changed = True + + if not changed: + continue + + try: + widget.field_value = current_text + widget.update() + except Exception as exc: + logger.warning( + "Failed to update PDF widget '%s' on page=%s: %s", + widget.field_name, + page_idx, + exc, + ) + + +def _prepare_signature_widget_ops( + doc: pymupdf.Document, + signature_widget_ops: dict[int, list[dict]], +) -> None: + """ + Flattens signature widgets and prepares their replacement operations. + + PyMuPDF bakes widgets at document scope, not per widget. When a + signature widget must be flattened, all widgets are intentionally baked + before sanitization so their visible appearances survive in the static + anonymized PDF. + + Args: + doc (pymupdf.Document): The PDF document being processed. + signature_widget_ops (dict[int, list[dict]]): The collected signature widget operations grouped by page index. + """ + should_bake_widgets = False + + for page_idx, ops in signature_widget_ops.items(): + if not ops: + continue + + page = doc[page_idx] + widgets = { + int(widget.xref): widget + for widget in (page.widgets() or []) + if widget.field_type == pymupdf.PDF_WIDGET_TYPE_SIGNATURE + } + grouped: dict[int, list[dict]] = {} + for op in ops: + grouped.setdefault(int(op["widget_xref"]), []).append(op) + + for widget_xref, widget_group_ops in grouped.items(): + widget = widgets.get(widget_xref) + widget_rect = pymupdf.Rect( + widget_group_ops[0].get("widget_rect") or (0, 0, 0, 0) + ) + + if widget is not None: + widget_rect = pymupdf.Rect(widget.rect) + should_bake_widgets = True + else: + logger.warning( + "Could not resolve PDF signature widget xref=%s on page=%s", + widget_xref, + page_idx, + ) + + for op in widget_group_ops: + op["widget_rect"] = pymupdf.Rect(widget_rect) + op.pop("asset_rect", None) + op.pop("image_rect", None) + op.pop("graphics_mode", None) + op["background_rect"] = _signature_background_rect(op, widget_rect) + + if should_bake_widgets: + try: + doc.bake(annots=False, widgets=True) + except Exception as exc: + logger.warning("Failed to flatten PDF signature widgets: %s", exc) diff --git a/aymurai/text/extraction.py b/aymurai/text/extraction.py index 66cc76b0..990eab6e 100644 --- a/aymurai/text/extraction.py +++ b/aymurai/text/extraction.py @@ -1,31 +1,16 @@ -import logging import mimetypes import os -import statistics -import unicodedata import zipfile from pathlib import Path -from typing import Any from zipfile import BadZipFile -import numpy as np -import pymupdf -import textract -import xmltodict -from lxml import etree -from more_itertools import flatten -from textract.exceptions import ShellError -from textract.parsers import _get_available_extensions - from aymurai.logger import get_logger -from aymurai.utils.misc import get_element, get_recursively +from aymurai.text.extractors import SUPPORTED_EXTENSIONS, InvalidFile, get_extractor logger = get_logger(__file__) -TEXTRACT_EXTENSIONS = _get_available_extensions() MIMETYPE_EXTENSION_MAPPER = { "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx", - "application/msword": "doc", "application/vnd.oasis.opendocument.text": "odt", "application/pdf": "pdf", } @@ -34,12 +19,6 @@ ERRORS = ["ignore", "coerce", "raise"] -class InvalidFile(Exception): - """Invalid File""" - - pass - - def _zip_contains(path: str, member: str) -> bool: """ Check if a zip file contains a specific member. @@ -100,133 +79,10 @@ def get_extension(path: str) -> str: return "unknown" -def _load_xml_from_odt(path: str, xmlfile: str = "styles.xml") -> str: - """ - Load xml file inside an odt. - - Args: - path (str): path to odt file. - xmlfile (str, optional): xml to open. Defaults to 'styles.xml'. - - Returns: - str: xml content. - """ - with zipfile.ZipFile(path, "r") as odt: - if xmlfile not in odt.namelist(): - return "" - with odt.open(xmlfile) as file: - content = file.read().decode("utf-8") - - return content - - -def _load_xml_from_docx(path: str, xmlfile: str = "word/footnotes.xml") -> Any | None: - """Extract XML content from a specific file inside a .docx.""" - with zipfile.ZipFile(path, "r") as docx: - if xmlfile not in docx.namelist(): - return - with docx.open(xmlfile) as f: - return etree.parse(f) - - -def get_header(path: str) -> list[str]: - """ - Extract header from styles.xml inside a ODT file. - - Args: - path (str): path to odt file. - - Returns: - list[str]: header lines. - """ - styles_xml_content = _load_xml_from_odt(path) - styles_dict = xmltodict.parse(styles_xml_content) - - header_root = get_element( - styles_dict, - levels=[ - "office:document-styles", - "office:master-styles", - "style:master-page", - ], - ) - - if not isinstance(header_root, list): - header_root = [header_root] - - style_header = [ - get_recursively(item, "style:header") - for item in header_root - if get_recursively(item, "style:header") - ] - style_header = list(flatten(style_header)) - - texts = [ - get_recursively(item, "#text") - for item in style_header - if get_recursively(item, "#text") - ] - texts = list(flatten(texts)) - - if not texts: - return [] - - return texts - - -def get_footnotes(path: str) -> list[str] | None: - """ - Extract footnotes from footnotes.xml inside a DOCX file. - - Args: - path (str): Path to the DOCX file. - - Returns: - list[str]: Footnote texts. - """ - footnotes_tree = _load_xml_from_docx(path) - if not footnotes_tree: - return - - footnotes_root = footnotes_tree.getroot() - - # Define the namespace map - ns = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"} - - # Extract footnote texts in order - footnotes_texts = [] - for footnote in footnotes_root.findall("w:footnote", namespaces=ns): - texts = footnote.xpath(".//w:t/text()", namespaces=ns) - if texts: - footnotes_texts.append("".join(texts)) - - return footnotes_texts - - -def pdf_to_text(filename: str, y_tolerance: float | None = None) -> str: - """ - Extract text from a PDF file. - - Args: - filename (str): Path to the PDF file. - y_tolerance (float, optional): - Maximum vertical gap (in points) to consider blocks part of the same paragraph. - - Returns: - str: Extracted text. - """ - if y_tolerance is None: - y_tolerance = compute_median_margin_between_blocks(filename) - - paragraphs = extract_and_merge_paragraphs(filename, np.ceil(y_tolerance)) - docu = "\n\n".join(paragraphs) - docu = unicodedata.normalize("NFKC", docu) - return docu - - def extract_document( filename: str | Path, errors: str = "ignore", + use_cache: bool = True, **kwargs, ) -> str | None: """ @@ -240,7 +96,8 @@ def extract_document( and warn. - If :const:`'ignore'`, then invalid parsing will be set as :const:`NaN` but not warn. - **kwargs: keyword arguments for textract. + use_cache (bool, optional): Toggle extractor-level caching. Defaults to True. + **kwargs: keyword arguments for text extractors. Raises: ValueError: Invalid argument. @@ -249,135 +106,41 @@ def extract_document( Returns: str: extracted document text. """ - filename = str(filename) # patch for pathlib + filename = str(filename) if errors not in ERRORS: raise ValueError(f"errors argument must be in {ERRORS}") ext = get_extension(filename) - kwargs["extension"] = kwargs.get("extension", ext) - kwargs["output_encoding"] = kwargs.get("output_encoding", "utf-8") - - logger = get_logger(f"{__file__}.{__name__}") - - if errors == "ignore": - logger.setLevel(logging.ERROR) - - if ( - not isinstance(filename, str) - or not os.path.exists(filename) - or ext not in TEXTRACT_EXTENSIONS - ): + if (not isinstance(filename, str)) or not os.path.exists(filename): if errors == "raise": raise InvalidFile(f"Invalid path: {filename}") - logger.warn(f"skipping (invalid): {filename}") - return - - try: - if ext == "pdf": - return pdf_to_text(filename, y_tolerance=kwargs.get("y_tolerance")) + logger.warning("Skipping (missing): %s", filename) + return None - docu = textract.process(filename, **kwargs).decode("utf-8") - except (BadZipFile, KeyError, ShellError): + if ext not in SUPPORTED_EXTENSIONS: if errors == "raise": - raise - logger.warn(f"skipping (corrupted): {filename}") - return - - # patch header loading in odt files - if ext == "odt": - header = "\n".join(get_header(filename)) - docu = header + "\n\n" + docu - - # patch footnotes loading in docx files - if ext == "docx": - footnotes = get_footnotes(filename) or [] - footnotes = "\n".join(footnotes) - if footnotes.strip(): - docu = docu + "\n\n" + footnotes - - docu = unicodedata.normalize("NFKC", docu) - return docu - - -def compute_median_margin_between_blocks(pdf_path: str) -> float: - """ - Computes the median vertical margin between text blocks in a PDF. - - Args: - pdf_path (str): Path to the PDF file. - - Returns: - float: Median margin between text blocks (in points). - """ - margins = [] - - with pymupdf.open(pdf_path) as doc: - for page in doc: - # Extract all text blocks from the page - blocks = page.get_text("blocks") - - # Sort blocks by their top y-coordinate (y0) - blocks_sorted = sorted(blocks, key=lambda b: b[1]) - - # Compute vertical margins between consecutive blocks - for i in range(1, len(blocks_sorted)): - previous_block = blocks_sorted[i - 1] - current_block = blocks_sorted[i] - - # Calculate the vertical margin - previous_y1 = previous_block[3] # Bottom of the previous block - current_y0 = current_block[1] # Top of the current block - margin = current_y0 - previous_y1 - - if margin > 0: # Ignore overlapping blocks - margins.append(margin) - - # Compute and return the median margin - if margins: - return statistics.median(margins) - else: - return 0.0 # Return 0 if no margins were found + raise InvalidFile(f"Unsupported extension: {ext}") + logger.warning("Skipping (unsupported %s): %s", ext, filename) + return None + extractor = get_extractor(ext) -def extract_and_merge_paragraphs(pdf_path: str, y_tolerance=5) -> list[str]: - """ - Extracts and merges paragraphs from a PDF by grouping close text blocks. - - Args: - pdf_path (str): Path to the PDF file. - y_tolerance (float): Maximum vertical gap (in points) to consider blocks part of the same paragraph. - - Returns: - list[str]: A list of merged paragraphs as strings. - """ - paragraphs = [] - current_paragraph = [] - last_y1 = None - - with pymupdf.open(pdf_path) as doc: - for page in doc: - # Extract all text blocks from the page - blocks = page.get_text("blocks") - - # Sort blocks by their top y-coordinate (y0) - blocks_sorted = sorted(blocks, key=lambda b: b[1]) - - for block in blocks_sorted: - x0, y0, x1, y1, text, *_ = block - - if last_y1 is not None and (y0 - last_y1) > y_tolerance: - # If the gap between blocks is too large, start a new paragraph - if current_paragraph: - paragraphs.append(" ".join(current_paragraph)) - current_paragraph = [] - - current_paragraph.append(text) - last_y1 = y1 - - if current_paragraph: - paragraphs.append(" ".join(current_paragraph)) - current_paragraph = [] - - return paragraphs + try: + return extractor.extract(Path(filename), use_cache=use_cache, **kwargs) + except InvalidFile as exc: + if errors == "raise": + raise + logger.warning("Skipping (corrupted): %s (%s)", filename, exc) + return None + except BadZipFile as exc: + if errors == "raise": + raise + logger.warning("Skipping (corrupted archive): %s (%s)", filename, exc) + return None + except Exception as exc: + if errors == "raise": + raise + logger.warning("Skipping (unexpected): %s (%s)", filename, exc) + return None diff --git a/aymurai/text/extractors/__init__.py b/aymurai/text/extractors/__init__.py new file mode 100644 index 00000000..01919448 --- /dev/null +++ b/aymurai/text/extractors/__init__.py @@ -0,0 +1,19 @@ +# Import concrete extractors so they self-register. +from aymurai.text.extractors import docx, odt, pdf # noqa: F401 +from aymurai.text.extractors.base import ( + BaseExtractor, + InvalidFile, + get_extractor, + register_extractor, + supported_extensions, +) + +SUPPORTED_EXTENSIONS = supported_extensions() + +__all__ = [ + "BaseExtractor", + "InvalidFile", + "SUPPORTED_EXTENSIONS", + "get_extractor", + "register_extractor", +] diff --git a/aymurai/text/extractors/base.py b/aymurai/text/extractors/base.py new file mode 100644 index 00000000..1603519d --- /dev/null +++ b/aymurai/text/extractors/base.py @@ -0,0 +1,113 @@ +from abc import ABC, abstractmethod +from pathlib import Path + +from aymurai.logger import get_logger + +logger = get_logger(__file__) + + +class InvalidFile(Exception): + """Raised when an extractor receives an invalid or missing file.""" + + +class BaseExtractor(ABC): + """Common interface shared by all document extractors.""" + + # Lowercase file extension handled by the extractor, without dot. + extension: str + + def ensure_file(self, path: Path) -> Path: + """ + Ensure the input file exists before extraction. + + Args: + path (Path): Candidate file path. + + Raises: + InvalidFile: If the file does not exist. + + Returns: + Path: Validated path ready for extraction. + """ + if not path.exists(): + raise InvalidFile(f"Invalid path: {path}") + return path + + @abstractmethod + def extract(self, path: Path, **kwargs) -> str: + """ + Extract normalized text from the source document. + + Args: + path (Path): Input document path. + **kwargs: Optional extractor-specific flags. + + Returns: + str: Cleaned textual content. + """ + + +_REGISTRY: dict[str, type[BaseExtractor]] = {} + + +def register_extractor(cls: type[BaseExtractor]) -> type[BaseExtractor]: + """ + Register an extractor class for a specific extension. + + Args: + cls (type[BaseExtractor]): Extractor class to register. + + Returns: + type[BaseExtractor]: Registered class for fluent decorator usage. + """ + extension = getattr(cls, "extension", None) + if not extension: + raise ValueError( + f"Extractor {cls.__name__} must define an 'extension' attribute" + ) + + normalized = extension.lower() + if normalized in _REGISTRY: + logger.warning( + "Overriding extractor for extension '%s' with %s", normalized, cls.__name__ + ) + + _REGISTRY[normalized] = cls + return cls + + +def get_extractor(extension: str) -> BaseExtractor: + """ + Retrieve an extractor instance for the desired extension. + + Args: + extension (str): File extension to resolve. + + Returns: + BaseExtractor: Ready-to-use extractor instance. + """ + normalized = extension.lower() + try: + extractor_cls = _REGISTRY[normalized] + except KeyError as exc: + raise ValueError(f"Unsupported extension: {extension}") from exc + return extractor_cls() + + +def supported_extensions() -> set[str]: + """ + List the registered document extensions. + + Returns: + set[str]: Known extensions handled by the registry. + """ + return set(_REGISTRY.keys()) + + +__all__ = [ + "BaseExtractor", + "InvalidFile", + "get_extractor", + "register_extractor", + "supported_extensions", +] diff --git a/aymurai/text/extractors/docx.py b/aymurai/text/extractors/docx.py new file mode 100644 index 00000000..52824449 --- /dev/null +++ b/aymurai/text/extractors/docx.py @@ -0,0 +1,31 @@ +from pathlib import Path +from typing import Any +from zipfile import BadZipFile + +import docx2txt + +from aymurai.text.extractors.base import BaseExtractor, InvalidFile, register_extractor +from aymurai.text.extractors.utils import get_footnotes, normalize_text + + +@register_extractor +class DocxExtractor(BaseExtractor): + extension = "docx" + + def extract(self, path: Path, **_: Any) -> str: + file_path = self.ensure_file(path) + + try: + document_text = docx2txt.process(str(file_path)) or "" + except (OSError, BadZipFile, KeyError) as exc: + raise InvalidFile(str(exc)) from exc + except Exception as exc: + raise InvalidFile(str(exc)) from exc + + footnotes = get_footnotes(file_path) or [] + footnotes_text = "\n".join(note for note in footnotes if note.strip()) + + if footnotes_text: + document_text = f"{document_text}\n\n{footnotes_text}" + + return normalize_text(document_text) diff --git a/aymurai/text/extractors/odt.py b/aymurai/text/extractors/odt.py new file mode 100644 index 00000000..41a55355 --- /dev/null +++ b/aymurai/text/extractors/odt.py @@ -0,0 +1,26 @@ +from pathlib import Path +from typing import Any +from xml.etree.ElementTree import ParseError +from zipfile import BadZipFile + +from aymurai.text.extractors.base import BaseExtractor, InvalidFile, register_extractor +from aymurai.text.extractors.utils import get_header, normalize_text, odt_to_text + + +@register_extractor +class OdtExtractor(BaseExtractor): + extension = "odt" + + def extract(self, path: Path, **_: Any) -> str: + file_path = self.ensure_file(path) + + try: + document_text = odt_to_text(file_path) + except (OSError, ValueError, BadZipFile, KeyError, ParseError) as exc: + raise InvalidFile(str(exc)) from exc + + header = "\n".join(get_header(file_path)).strip() + if header: + document_text = f"{header}\n\n{document_text}" + + return normalize_text(document_text) diff --git a/aymurai/text/extractors/pdf.py b/aymurai/text/extractors/pdf.py new file mode 100644 index 00000000..c672dfe7 --- /dev/null +++ b/aymurai/text/extractors/pdf.py @@ -0,0 +1,20 @@ +from pathlib import Path +from typing import Any + +from aymurai.text.extractors.base import BaseExtractor, InvalidFile, register_extractor +from aymurai.text.extractors.utils import pdf_to_text + + +@register_extractor +class PdfExtractor(BaseExtractor): + extension = "pdf" + + def extract(self, path: Path, **_: Any) -> str: + file_path = self.ensure_file(path) + + try: + return pdf_to_text(file_path) + except (OSError, ValueError) as exc: + raise InvalidFile(str(exc)) from exc + except Exception as exc: + raise InvalidFile(str(exc)) from exc diff --git a/aymurai/text/extractors/utils.py b/aymurai/text/extractors/utils.py new file mode 100644 index 00000000..8db4c661 --- /dev/null +++ b/aymurai/text/extractors/utils.py @@ -0,0 +1,280 @@ +import re +import unicodedata +import xml.etree.ElementTree as ET +import zipfile +from pathlib import Path +from typing import AbstractSet, Any + +import pymupdf +import pymupdf.layout # noqa: F401 # activates layout support +import pymupdf4llm +import xmltodict +from lxml import etree +from more_itertools import flatten + +from aymurai.logger import get_logger +from aymurai.utils.misc import get_element, get_recursively + +logger = get_logger(__file__) + + +ODT_NS = {"text": "urn:oasis:names:tc:opendocument:xmlns:text:1.0"} +PDF_SKIP_BOX_CLASSES = frozenset({"picture", "formula", "table"}) + + +def normalize_text(text: str) -> str: + """ + Normalize Unicode output consistently across extractors. + + Args: + text (str): Raw text extracted from a document. + + Returns: + str: Normalized string in NFKC form. + """ + return unicodedata.normalize("NFKC", text) + + +def _clean_pdf_box_text(text: str, box_class: str) -> str: + """ + Clean box-level PDF text while preserving the original layout content. + + Args: + text (str): Raw text sliced from a page box. + box_class (str): Box class emitted by ``pymupdf4llm``. + + Returns: + str: Cleaned, normalized box text. + """ + text = normalize_text(text).strip() + if box_class == "footnote": + text = re.sub(r"(?m)^>\s?", "", text) + return text + + +def pdf_to_paragraphs( + file_path: Path | str, + *, + include_headers: bool = True, + include_footers: bool = True, + skip_box_classes: AbstractSet[str] = PDF_SKIP_BOX_CLASSES, +) -> list[str]: + """ + Extract paragraph-like layout units from a PDF using PyMuPDF layout parsing. + + Args: + file_path (Path | str): Path to the PDF document. + include_headers (bool): Whether to keep header boxes. Defaults to True. + include_footers (bool): Whether to keep footer boxes. Defaults to True. + skip_box_classes (AbstractSet[str]): Layout box classes to ignore. Defaults to PDF_SKIP_BOX_CLASSES. + + Returns: + list[str]: Normalized paragraph strings extracted from the PDF. + """ + logger.debug("Extracting layout paragraphs from PDF: %s", file_path) + + with pymupdf.open(str(file_path)) as doc: + chunks = pymupdf4llm.to_text( + doc, + filename=str(file_path), + page_chunks=True, + header=include_headers, + footer=include_footers, + show_progress=False, + force_text=True, + use_ocr=False, + force_ocr=False, + ) + + paragraphs: list[str] = [] + for chunk in chunks: + page_text = chunk.get("text") or "" + for box in chunk.get("page_boxes") or []: + if box.get("class") in skip_box_classes: + continue + + start, stop = box.get("pos", (0, 0)) + text = _clean_pdf_box_text(page_text[start:stop], box.get("class") or "") + if text: + paragraphs.append(text) + + return paragraphs + + +def pdf_to_text(file_path: Path | str) -> str: + """ + Extract normalized plain text from a PDF using filtered layout boxes. + + Args: + file_path (Path | str): Path to the PDF document. + + Returns: + str: Cleaned textual content extracted from the PDF. + """ + return "\n\n".join(pdf_to_paragraphs(file_path)) + + +def load_xml_from_docx(path: Path, xmlfile: str = "word/footnotes.xml") -> Any | None: + """ + Extract XML content from a specific file inside a DOCX container. + + Args: + path (Path): DOCX archive path. + xmlfile (str, optional): Internal member name to inspect. Defaults to "word/footnotes.xml". + + Returns: + Any | None: Parsed XML tree or None when the member is missing. + """ + with zipfile.ZipFile(path, "r") as docx: + if xmlfile not in docx.namelist(): + return None + with docx.open(xmlfile) as handle: + return etree.parse(handle) + + +def load_xml_from_odt(path: Path, xmlfile: str = "styles.xml") -> str: + """ + Load XML content from an ODT archive member. + + Args: + path (Path): ODT archive path. + xmlfile (str, optional): Member name to open. Defaults to "styles.xml". + + Returns: + str: UTF-8 decoded XML content or an empty string when absent. + """ + with zipfile.ZipFile(path, "r") as odt: + if xmlfile not in odt.namelist(): + return "" + with odt.open(xmlfile) as file: + return file.read().decode("utf-8") + + +def get_header(path: Path) -> list[str]: + """ + Extract header text defined in an ODT stylesheet. + + Args: + path (Path): ODT document path. + + Returns: + list[str]: Header snippets collected from the stylesheet. + """ + styles_xml_content = load_xml_from_odt(path) + if not styles_xml_content: + return [] + + styles_dict = xmltodict.parse(styles_xml_content) + + header_root = get_element( + styles_dict, + levels=[ + "office:document-styles", + "office:master-styles", + "style:master-page", + ], + ) + + if not isinstance(header_root, list): + header_root = [header_root] + + style_header = [ + get_recursively(item, "style:header") + for item in header_root + if get_recursively(item, "style:header") + ] + style_header = list(flatten(style_header)) + + texts = [ + get_recursively(item, "#text") + for item in style_header + if get_recursively(item, "#text") + ] + texts = list(flatten(texts)) + + return [text for text in texts if isinstance(text, str)] + + +def get_footnotes(path: Path) -> list[str] | None: + """ + Extract footnotes from a DOCX document. + + Args: + path (Path): DOCX document path. + + Returns: + list[str] | None: Ordered footnote texts or None when absent. + """ + footnotes_tree = load_xml_from_docx(path) + if not footnotes_tree: + return None + + footnotes_root = footnotes_tree.getroot() + ns = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"} + + footnotes_texts: list[str] = [] + for footnote in footnotes_root.findall("w:footnote", namespaces=ns): + texts = footnote.xpath(".//w:t/text()", namespaces=ns) + if texts: + footnotes_texts.append("".join(texts)) + + return footnotes_texts + + +def _odt_qn(name: str) -> str: + """ + Resolve a qualified ODT tag into the ElementTree namespace form. + + Args: + name (str): Namespaced tag in the ``prefix:local`` format. + + Returns: + str: Expanded tag with namespace URI. + """ + prefix, local = name.split(":", 1) + return f"{{{ODT_NS[prefix]}}}{local}" + + +def _odt_text_to_string(elem) -> str: + """ + Flatten an ODT Element into a plain-text string, preserving special tokens. + + Args: + elem (Element): XML element to flatten. + + Returns: + str: Concatenated text content for the element subtree. + """ + out = elem.text or "" + for child in elem: + if child.tag == _odt_qn("text:tab"): + out += "\t" + elif child.tag == _odt_qn("text:s"): + out += " " * int(child.get(_odt_qn("text:c"), 1)) + else: + out += _odt_text_to_string(child) + if child.tail: + out += child.tail + return out + + +def odt_to_text(path: Path) -> str: + """ + Extract text content, preserving paragraphs and headings, from an ODT file. + + Args: + path (Path): ODT document path. + + Returns: + str: Plain text version of the document content. + """ + with zipfile.ZipFile(path, "r") as archive: + content_xml = archive.read("content.xml") + + content = ET.fromstring(content_xml) + + lines = [] + for child in content.iter(): + if child.tag in (_odt_qn("text:p"), _odt_qn("text:h")): + lines.append(_odt_text_to_string(child)) + return "\n".join(lines) diff --git a/aymurai/text/normalize.py b/aymurai/text/normalize.py index 9027a0d8..4154533b 100644 --- a/aymurai/text/normalize.py +++ b/aymurai/text/normalize.py @@ -2,45 +2,72 @@ import unicodedata -def document_normalize(text: str) -> str: - """Normalize extracted text from documents - * join invalid newlines - * remove continous whitespaces +def _normalize_document_characters(text: str) -> str: + """ + Apply character-level normalization without changing document structure. Args: - text (str): document + text (str): Raw extracted document text. Returns: - str: normalized + str: Character-normalized text. """ - - # normalize character encodings - # text = unicodedata.normalize("NFKD", text) + text = text.replace("\r\n", "\n").replace("\r", "\n") text = unicodedata.normalize("NFKC", text) + text = re.sub(r"(“|”)", '"', text) + text = text.replace("\\/", "/") + text = re.sub(r"[ \t]{2,}", " ", text) + return text + - # remove continous whitespace - text = re.sub(r" {2,}", r" ", text) +def _normalize_paragraph_text(text: str) -> str: + """ + Normalize text inside a single paragraph while preserving paragraph borders. + + Args: + text (str): Paragraph text. + + Returns: + str: Normalized paragraph content. + """ + text = re.sub(r"[ \t]*\n[ \t]*", "\n", text.strip()) # delete newline if NEXT char is: # - lower character or a number - # - punctuanion + # - punctuation text = re.sub(r"\n([a-z0-9;:,\.])", r" \g<1>", text) # delete newline if PREVIOUS char is: # - quote mark - # - punctuanions (except '.' because possible ambiguity) + # - punctuations (except '.' because possible ambiguity) text = re.sub(r"([\w,\"-])\n", r"\g<1> ", text) # cleanup some junk - # - multiple newlines, hyphens - text = re.sub(r"\n{2,}", "\n", text) text = re.sub(r"[-]{2,}", "-", text) text = re.sub(r"\.-", ".", text) + text = re.sub(r" {2,}", " ", text) + return text.strip() - # quotation marks - text = re.sub(r"(“|”)", '"', text) - # scaped slashes - text = text.replace("\/", "/") +def document_normalize(text: str, *, preserve_paragraphs: bool = False) -> str: + """Normalize extracted text from documents. - return text + Args: + text (str): Document text. + preserve_paragraphs (bool): Preserve blank-line paragraph boundaries. Defaults to False. + + Returns: + str: Normalized document text. + """ + text = _normalize_document_characters(text) + + if preserve_paragraphs: + paragraphs = [ + _normalize_paragraph_text(paragraph) + for paragraph in re.split(r"\n\s*\n+", text) + if paragraph.strip() + ] + return "\n\n".join(paragraphs) + + text = _normalize_paragraph_text(text) + return re.sub(r"\n{2,}", "\n", text) diff --git a/aymurai/text/tokenizers/spanish.py b/aymurai/text/tokenizers/spanish.py deleted file mode 100644 index fa11b0b4..00000000 --- a/aymurai/text/tokenizers/spanish.py +++ /dev/null @@ -1,372 +0,0 @@ -""" -A simplified clone of spaCy’s Spanish tokenizer (and related components) -without any dependency on spaCy. This module provides: - • TOKENIZER_EXCEPTIONS – a mapping for known exceptions. - • STOP_WORDS – Spanish stop words. - • TOKENIZER_SUFFIXES and TOKENIZER_INFIXES – basic regex rules. - • Lexical attribute getter for “like_num”. - • SpanishTokenizer – a class that tokenizes text using the above rules. - -Note: This is a “simple but close” clone. spaCy’s internal rules are very -comprehensive; here we implement only some of the core ideas. -""" - -import re - -# --- Definitions used in spaCy’s internals, simplified --- -ORTH = "ORTH" -NORM = "NORM" - - -def update_exc(base, new_exc): - base.update(new_exc) - return base - - -# A base (empty) exceptions dict -BASE_EXCEPTIONS = {} - -# --- Tokenizer Exceptions (from spacy.lang.es.tokenizer_exceptions) --- -_exc = { - "pal": [{ORTH: "pa"}, {ORTH: "l", NORM: "el"}], -} - -for exc_data in [ - {ORTH: "n°"}, - {ORTH: "°C"}, - {ORTH: "aprox."}, - {ORTH: "dna."}, - {ORTH: "dpto."}, - {ORTH: "ej."}, - {ORTH: "esq."}, - {ORTH: "pág."}, - {ORTH: "p.ej."}, - {ORTH: "Ud.", NORM: "usted"}, - {ORTH: "Vd.", NORM: "usted"}, - {ORTH: "Uds.", NORM: "ustedes"}, - {ORTH: "Vds.", NORM: "ustedes"}, - {ORTH: "vol.", NORM: "volúmen"}, -]: - _exc[exc_data[ORTH]] = [exc_data] - -# Times exceptions: “12m.” splits into “12” and “m.”, and also the h+a.m./p.m. forms. -_exc["12m."] = [{ORTH: "12"}, {ORTH: "m."}] - -for h in range(1, 13): - for period in ["a.m.", "am"]: - _exc[f"{h}{period}"] = [{ORTH: f"{h}"}, {ORTH: period}] - for period in ["p.m.", "pm"]: - _exc[f"{h}{period}"] = [{ORTH: f"{h}"}, {ORTH: period}] - -for orth in [ - "a.C.", - "a.J.C.", - "d.C.", - "d.J.C.", - "apdo.", - "Av.", - "Avda.", - "Cía.", - "Dr.", - "Dra.", - "EE.UU.", - "Ee.Uu.", - "EE. UU.", - "Ee. Uu.", - "etc.", - "fig.", - "Gob.", - "Gral.", - "Ing.", - "J.C.", - "km/h", - "Lic.", - "m.n.", - "núm.", - "P.D.", - "Prof.", - "Profa.", - "q.e.p.d.", - "Q.E.P.D.", - "S.A.", - "S.L.", - "S.R.L.", - "s.s.s.", - "Sr.", - "Sra.", - "Srta.", -]: - _exc[orth] = [{ORTH: orth}] - -TOKENIZER_EXCEPTIONS = update_exc(BASE_EXCEPTIONS.copy(), _exc) - -# --- Stop words (from spacy.lang.es.stop_words) --- -STOP_WORDS = set( - """ -a acuerdo adelante ademas además afirmó agregó ahi ahora ahí al algo alguna -algunas alguno algunos algún alli allí alrededor ambos ante anterior antes -apenas aproximadamente aquel aquella aquellas aquello aquellos aqui aquél -aquélla aquéllas aquéllos aquí arriba aseguró asi así atras aun aunque añadió -aún - -bajo bastante bien breve buen buena buenas bueno buenos - -cada casi cierta ciertas cierto ciertos cinco claro comentó como con conmigo -conocer conseguimos conseguir considera consideró consigo consigue consiguen -consigues contigo contra creo cual cuales cualquier cuando cuanta cuantas -cuanto cuantos cuatro cuenta cuál cuáles cuándo cuánta cuántas cuánto cuántos -cómo - -da dado dan dar de debajo debe deben debido decir dejó del delante demasiado -demás dentro deprisa desde despacio despues después detras detrás dia dias dice -dicen dicho dieron diez diferente diferentes dijeron dijo dio doce donde dos -durante día días dónde - -e el ella ellas ello ellos embargo en encima encuentra enfrente enseguida -entonces entre era eramos eran eras eres es esa esas ese eso esos esta estaba -estaban estado estados estais estamos estan estar estará estas este esto estos -estoy estuvo está están excepto existe existen explicó expresó él ésa ésas ése -ésos ésta éstas éste éstos - -fin final fue fuera fueron fui fuimos - -gran grande grandes - -ha haber habia habla hablan habrá había habían hace haceis hacemos hacen hacer -hacerlo haces hacia haciendo hago han hasta hay haya he hecho hemos hicieron -hizo hoy hubo - -igual incluso indicó informo informó ir - -junto - -la lado largo las le les llegó lleva llevar lo los luego - -mal manera manifestó mas mayor me mediante medio mejor mencionó menos menudo mi -mia mias mientras mio mios mis misma mismas mismo mismos modo mucha muchas -mucho muchos muy más mí mía mías mío míos - -nada nadie ni ninguna ningunas ninguno ningunos ningún no nos nosotras nosotros -nuestra nuestras nuestro nuestros nueva nuevas nueve nuevo nuevos nunca - -o ocho once os otra otras otro otros - -para parece parte partir pasada pasado paìs peor pero pesar poca pocas poco -pocos podeis podemos poder podria podriais podriamos podrian podrias podrá -podrán podría podrían poner por porque posible primer primera primero primeros -pronto propia propias propio propios proximo próximo próximos pudo pueda puede -pueden puedo pues - -qeu que quedó queremos quien quienes quiere quiza quizas quizá quizás quién -quiénes qué - -realizado realizar realizó repente respecto - -sabe sabeis sabemos saben saber sabes salvo se sea sean segun segunda segundo -según seis ser sera será serán sería señaló si sido siempre siendo siete sigue -siguiente sin sino sobre sois sola solamente solas solo solos somos son soy su -supuesto sus suya suyas suyo suyos sé sí sólo - -tal tambien también tampoco tan tanto tarde te temprano tendrá tendrán teneis -tenemos tener tenga tengo tenido tenía tercera tercero ti tiene tienen toda -todas todavia todavía todo todos total tras trata través tres tu tus tuvo tuya -tuyas tuyo tuyos tú - -u ultimo un una unas uno unos usa usais usamos usan usar usas uso usted ustedes -última últimas último últimos - -va vais vamos van varias varios vaya veces ver verdad verdadera verdadero vez -vosotras vosotros voy vuestra vuestras vuestro vuestros - -y ya yo -""".split() -) - -# --- Punctuation-related rules (a simplified version of spacy.lang.es.punctuation) --- -# Here we provide basic suffix and infix patterns. -TOKENIZER_SUFFIXES = [ - r"[—–]", # dashes - r"[.,!?;:%]", # common punctuation - r"['\"“”‘’]", # quotes -] -# Infixes that split on, for example, hyphens between digits. -TOKENIZER_INFIXES = [ - r"(?<=[0-9])[-/](?=[0-9])", -] - -# --- Lexical attribute getter (from spacy.lang.es.lex_attrs) --- -_num_words = { - "cero", - "uno", - "dos", - "tres", - "cuatro", - "cinco", - "seis", - "siete", - "ocho", - "nueve", - "diez", - "once", - "doce", - "trece", - "catorce", - "quince", - "dieciséis", - "diecisiete", - "dieciocho", - "diecinueve", - "veinte", - "veintiuno", - "veintidós", - "veintitrés", - "veinticuatro", - "veinticinco", - "veintiséis", - "veintisiete", - "veintiocho", - "veintinueve", - "treinta", - "cuarenta", - "cincuenta", - "sesenta", - "setenta", - "ochenta", - "noventa", - "cien", - "mil", - "millón", - "billón", - "trillón", -} -_ordinal_words = { - "primero", - "segundo", - "tercero", - "cuarto", - "quinto", - "sexto", - "séptimo", - "octavo", - "noveno", - "décimo", - "undécimo", - "duodécimo", - "decimotercero", - "decimocuarto", - "decimoquinto", - "decimosexto", - "decimoséptimo", - "decimoctavo", - "decimonoveno", - "vigésimo", - "trigésimo", - "cuadragésimo", - "quincuagésimo", - "sexagésimo", - "septuagésimo", - "octogésima", - "nonagésima", - "centésima", - "milésima", - "millonésima", - "billonésima", -} - - -def like_num(text): - # Remove a leading +, -, ±, or ~ if present. - if text.startswith(("+", "-", "±", "~")): - text = text[1:] - # Remove commas and periods for a digit check. - text_clean = text.replace(",", "").replace(".", "") - if text_clean.isdigit(): - return True - if text.count("/") == 1: - num, denom = text.split("/") - if num.isdigit() and denom.isdigit(): - return True - text_lower = text.lower() - if text_lower in _num_words: - return True - if text_lower in _ordinal_words: - return True - return False - - -LEX_ATTRS = {"LIKE_NUM": like_num} - - -# --- Spanish Tokenizer Implementation --- -class SpanishTokenizer: - """ - Spacy spanish tokenizer clone - Source: https://github.com/explosion/spaCy/tree/b3c46c315eb16ce644bddd106d31c3dd349f6bb2/spacy/lang/es - """ - - def __init__(self): - # A basic regex for “words” and “non-space” characters. - self.word_re = re.compile(r"\w+|[^\w\s]", re.UNICODE) - # Combine infix patterns into one regex. - self.infix_re = re.compile("|".join(TOKENIZER_INFIXES)) - - def apply_exceptions(self, text: str): - """ - If the given text exactly matches a tokenizer exception, - return the corresponding token list. - """ - if text in TOKENIZER_EXCEPTIONS: - # For each exception item (a dict), return the ORTH value, - # falling back to NORM if available. - return [ - item.get(ORTH, item.get(NORM, item)) - for item in TOKENIZER_EXCEPTIONS[text] - ] - return None - - def tokenize(self, text: str): - """ - Tokenizes the input text into a list of tokens. - The algorithm first splits on whitespace, checks for exceptions, - and otherwise applies regex-based splitting and infix splitting. - """ - tokens = [] - for word in text.split(): - exc = self.apply_exceptions(word) - if exc is not None: - tokens.extend(exc) - else: - # First, use regex to split word into basic tokens. - sub_tokens = self.word_re.findall(word) - final_tokens = [] - for token in sub_tokens: - # Apply infix splitting if applicable. - splits = self.infix_re.split(token) - if len(splits) > 1: - last_end = 0 - # Re-find infix matches to capture separators. - for m in self.infix_re.finditer(token): - pre = token[last_end : m.start()] - sep = token[m.start() : m.end()] - if pre: - final_tokens.append(pre) - final_tokens.append(sep) - last_end = m.end() - remainder = token[last_end:] - if remainder: - final_tokens.append(remainder) - else: - final_tokens.append(token) - tokens.extend(final_tokens) - return tokens - - def __call__(self, text: str): - return self.tokenize(text) - - -# --- Example Usage --- -if __name__ == "__main__": - sample_text = "El Sr. habló con 12a.m. y 3pm en la reunión. ¡Increíble, verdad?" - tokenizer = SpanishTokenizer() - tokens = tokenizer(sample_text) - print("Tokens:", tokens) diff --git a/aymurai/transforms/anonymization_postprocess/core.py b/aymurai/transforms/anonymization_postprocess/core.py index 977fb109..af5185e4 100644 --- a/aymurai/transforms/anonymization_postprocess/core.py +++ b/aymurai/transforms/anonymization_postprocess/core.py @@ -1,10 +1,10 @@ import re from copy import deepcopy -from string import punctuation +from aymurai.meta.pipeline_interfaces import Transform from aymurai.meta.types import DataItem from aymurai.utils.misc import get_element -from aymurai.meta.pipeline_interfaces import Transform +from aymurai.transforms.anonymization_postprocess.exact_labels import EXACT_LABELS class AnonymizationEntityCleaner(Transform): @@ -33,6 +33,7 @@ def process(self, ent: dict) -> dict: original_text = ent["text"] start_char = ent["start_char"] end_char = ent["end_char"] + label = ent["attrs"]["aymurai_label"] # Match leading and trailing non-alphanumeric characters leading_match = re.match(r"^\W+", original_text) @@ -45,10 +46,27 @@ def process(self, ent: dict) -> dict: # Clean the text cleaned_text = pattern.sub("", original_text) + if not cleaned_text: + return None + + raw_subclass = ent["attrs"]["aymurai_label_subclass"] + if isinstance(raw_subclass, list): + aymurai_label_subclass = raw_subclass.copy() + elif raw_subclass: + aymurai_label_subclass = [raw_subclass] + else: + aymurai_label_subclass = [] + + if label in EXACT_LABELS: + flattened_text = re.sub(r"[^a-zA-Z0-9]", "", cleaned_text) + if flattened_text and flattened_text not in aymurai_label_subclass: + aymurai_label_subclass.append(flattened_text) + # Update the entity's alt text and indices ent["attrs"]["aymurai_alt_text"] = cleaned_text ent["attrs"]["aymurai_alt_start_char"] = start_char + leading_chars_removed ent["attrs"]["aymurai_alt_end_char"] = end_char - trailing_chars_removed + ent["attrs"]["aymurai_label_subclass"] = aymurai_label_subclass return ent @@ -61,11 +79,11 @@ def __call__(self, item: DataItem) -> DataItem: DataItem: processed item """ item = deepcopy(item) - ents = get_element(item, [self.field, "entities"]) or [] - # Filter out predictions that are punctuation marks only - ents = [ent for ent in ents if ent["text"] not in punctuation] - ents = [self.process(ent) for ent in ents] + # Filter out predictions with empty alt text and update the rest + item[self.field]["entities"] = [ + out for ent in ents if (out := self.process(ent)) is not None + ] return item diff --git a/aymurai/transforms/anonymization_postprocess/exact_labels.py b/aymurai/transforms/anonymization_postprocess/exact_labels.py new file mode 100644 index 00000000..afe345da --- /dev/null +++ b/aymurai/transforms/anonymization_postprocess/exact_labels.py @@ -0,0 +1,10 @@ +EXACT_LABELS = { + "DNI", + "CUIT_CUIL", + "TELEFONO", + "PATENTE_DOMINIO", + "IP", + "NUM_CAJA_AHORRO", + "CBU", + "NUM_MATRICULA", +} diff --git a/aymurai/transforms/datetime_formatter/__init__.py b/aymurai/transforms/datetime_formatter/__init__.py index e69de29b..822a1d04 100644 --- a/aymurai/transforms/datetime_formatter/__init__.py +++ b/aymurai/transforms/datetime_formatter/__init__.py @@ -0,0 +1 @@ +from .core import DatetimeFormatter diff --git a/aymurai/transforms/datetime_formatter/core.py b/aymurai/transforms/datetime_formatter/core.py index 8f3c49c9..e98c0d76 100644 --- a/aymurai/transforms/datetime_formatter/core.py +++ b/aymurai/transforms/datetime_formatter/core.py @@ -2,9 +2,9 @@ from datetime_matcher import DatetimeMatcher +from aymurai.meta.pipeline_interfaces import Transform from aymurai.meta.types import DataItem from aymurai.utils.misc import get_element -from aymurai.meta.pipeline_interfaces import Transform from .patterns import patterns @@ -55,7 +55,9 @@ def process(self, ent): text_repr = datetime.strftime("%d/%m/%Y") suggestions.append(text_repr) - ent["attrs"]["aymurai_label_subclass"] = suggestions + ent["attrs"]["aymurai_label_subclass"] = ( + [max(suggestions)] if suggestions else [] + ) return ent diff --git a/aymurai/transforms/datetime_formatter/patterns.py b/aymurai/transforms/datetime_formatter/patterns.py index 77bf1d9d..242b04e4 100644 --- a/aymurai/transforms/datetime_formatter/patterns.py +++ b/aymurai/transforms/datetime_formatter/patterns.py @@ -10,7 +10,13 @@ r"%d/%m/%y", r"(?i)%-d de %B del? %Y", r"(?i)%-d de %B %Y", + r"(?i)%-d %B de %Y", + r"(?i)%-d de %B %Y", + r"%d-%m-%Y", + r"%Y-%m-%d", + r"(?i)%-d de %B", ] + HOURS = [ r"%H[\.:]%M", r"(?i)%-H[\.:]%M horas", @@ -19,6 +25,7 @@ patterns = { "FECHA_RESOLUCION": DATES, + "FECHA": DATES, "HORA_DE_INICIO": HOURS, "HORA_DE_CIERRE": HOURS, } diff --git a/aymurai/transforms/entity_subcategories/bm25.py b/aymurai/transforms/entity_subcategories/bm25.py new file mode 100644 index 00000000..cb88d145 --- /dev/null +++ b/aymurai/transforms/entity_subcategories/bm25.py @@ -0,0 +1,101 @@ +import re +from typing import Callable + +import numpy as np + + +class BM25Scorer: + """Lightweight BM25 scorer over subcategory strings.""" + + def __init__(self, subcategories: list[str], normalize_fn: Callable[[str], str]): + """ + Initialize the BM25Scorer with subcategories and a normalization function. + + Args: + subcategories (list[str]): List of subcategory strings to be scored. + normalize_fn (Callable[[str], str]): Function to normalize text before tokenization. + """ + self.subcategories = subcategories + self.token_pattern = re.compile(r"\w+", re.UNICODE) + self.normalize_fn = normalize_fn + + tokenized = [self._tokenize(sub) for sub in subcategories] + self.doc_len = [len(toks) for toks in tokenized] + self.avgdl = sum(self.doc_len) / len(self.doc_len) if self.doc_len else 0.0 + self.N = len(tokenized) + self.doc_freqs = [self._to_counter(toks) for toks in tokenized] + corpus_tokens = set().union(*self.doc_freqs) if self.doc_freqs else set() + self.df = { + token: sum(1 for doc in self.doc_freqs if token in doc) + for token in corpus_tokens + } + # Using BM25+ style IDF smoothing + self.idf = { + token: np.log(1 + (self.N - freq + 0.5) / (freq + 0.5)) + for token, freq in self.df.items() + } + + def _tokenize(self, text: str) -> list[str]: + """ + Tokenize the input text after normalization. + + Args: + text (str): Input text to tokenize. + + Returns: + list[str]: List of tokens. + """ + normalized = self.normalize_fn(text) + return self.token_pattern.findall(normalized) + + def _to_counter(self, tokens: list[str]) -> dict[str, int]: + """ + Convert a list of tokens into a frequency counter. + + Args: + tokens (list[str]): List of tokens. + + Returns: + dict[str, int]: Frequency counter of tokens. + """ + counts = {} + + for t in tokens: + counts[t] = counts.get(t, 0) + 1 + + return counts + + def score_vector(self, text: str, k1: float = 1.2, b: float = 0.75) -> np.ndarray: + """ + Compute BM25 scores for the input text against all subcategories. + + Args: + text (str): Input text to score. + k1 (float, optional): BM25 k1 parameter. Defaults to 1.2. + b (float, optional): BM25 b parameter. Defaults to 0.75. + + Returns: + np.ndarray: + """ + scores = np.zeros(self.N, dtype=np.float64) + tokens = self._tokenize(text) + + for token in tokens: + idf = self.idf.get(token) + if idf is None: + continue + + for idx, freq_map in enumerate(self.doc_freqs): + freq = freq_map.get(token, 0) + if freq == 0: + continue + + # BM25 scoring formula + denom = freq + k1 * ( + 1 - b + b * self.doc_len[idx] / (self.avgdl + 1e-9) + ) + + # Update score + scores[idx] += idf * freq * (k1 + 1) / denom + + return scores diff --git a/aymurai/transforms/entity_subcategories/sentence_transformer.py b/aymurai/transforms/entity_subcategories/sentence_transformer.py new file mode 100644 index 00000000..790127f2 --- /dev/null +++ b/aymurai/transforms/entity_subcategories/sentence_transformer.py @@ -0,0 +1,268 @@ +from copy import deepcopy +from pathlib import Path +from typing import Iterable + +import numpy as np + +from aymurai.logger import get_logger +from aymurai.meta.pipeline_interfaces import Transform +from aymurai.meta.types import DataItem +from aymurai.models.sentence_encoder.base import BaseSentenceEncoder +from aymurai.models.sentence_encoder.factory import create_encoder +from aymurai.settings import settings +from aymurai.transforms.entity_subcategories.bm25 import BM25Scorer +from aymurai.transforms.entity_subcategories.subcategories import SUBCATEGORIES +from aymurai.transforms.entity_subcategories.utils import filter_by_category +from aymurai.utils.misc import get_element + +logger = get_logger(__name__) + + +class SentenceTransformerSubcategorizer(Transform): + """ + Hybrid sentence-transformer + optional BM25 subcategorizer. + + - When ``bm25_weight > 0`` mixes BM25 and encoder cosine scores. + - When ``bm25_weight <= 0`` it falls back to encoder-only retrieval. + - Subcategories are normalized (underscore -> space, then encoder.normalize_text) before encoding. + """ + + def __init__( + self, + category: str, + embeddings_path: str, + encoder_name: str = "distiluse", + bm25_weight: float = 0.5, + device: str | None = None, + batch_size: int = 256, + rebuild_embeddings: bool = False, + encoder: BaseSentenceEncoder | None = None, + ): + """ + SentenceTransformerSubcategorizer constructor. + + Args: + category (str): Category to subcategorize. + embeddings_path (str): Path to store/load subcategory embeddings. + encoder_name (str, optional): Name of the encoder to use. Defaults to "distiluse". + bm25_weight (float, optional): Weight for BM25 scoring. Defaults to 0.5. + device (str | None, optional): Device to run the encoder on. Defaults to None. + batch_size (int, optional): Batch size for encoding. Defaults to 256. + rebuild_embeddings (bool, optional): Whether to rebuild embeddings even if cached ones exist. Defaults to False. + encoder (BaseSentenceEncoder | None, optional): Encoder instance to use. Defaults to None. + """ + self.category = category + self.bm25_weight = float(bm25_weight) + self.batch_size = batch_size + + cache_root = Path(settings.CACHE_BASEPATH) + self.cache_path = cache_root / self.__class__.__name__ + self.cache_path.mkdir(parents=True, exist_ok=True) + + self.subcategories = self._load_subcategories(category) + + self.encoder_name = encoder_name + self.encoder = encoder or create_encoder( + encoder_type=encoder_name, device=device + ) + + embeddings_path = Path(embeddings_path) + if not embeddings_path.is_absolute(): + embeddings_path = self.cache_path / embeddings_path + embeddings_path.parent.mkdir(parents=True, exist_ok=True) + self.embeddings_path = embeddings_path + + self.response_vectors = self._load_or_build_embeddings(rebuild_embeddings) + self.bm25 = ( + BM25Scorer(self.subcategories, normalize_fn=self._normalize_subcategory) + if self.bm25_weight > 0 + else None + ) + + def _load_subcategories(self, category: str) -> list[str]: + """ + Load subcategories for a given category. + + Args: + category (str): Category name. + + Raises: + ValueError: If no subcategories are found for the given category. + + Returns: + list[str]: List of subcategories. + """ + key = category.lower().replace(" ", "_") + try: + return SUBCATEGORIES[key] + except KeyError as exc: + raise ValueError( + f"No subcategories found for category '{category}'" + ) from exc + + def _load_or_build_embeddings(self, rebuild: bool) -> np.ndarray: + """ + Load or build subcategory response embeddings. + + Args: + rebuild (bool): Whether to rebuild embeddings even if cached ones exist. + + Raises: + ValueError: If the embeddings file is missing expected data. + + Returns: + np.ndarray: Array of subcategory embeddings. + """ + if self.embeddings_path.exists() and not rebuild: + data = np.load(self.embeddings_path, allow_pickle=True) + if isinstance(data, np.lib.npyio.NpzFile): + vectors = data.get("vectors") + subs = data.get("subcategories") + if vectors is None: + raise ValueError("Embeddings file missing 'vectors'") + if subs is not None and len(subs) != len(self.subcategories): + logger.warning("Subcategory count mismatch; rebuilding embeddings") + else: + return vectors + else: + return data + + logger.info("Building response embeddings for subcategories") + + # Apply the same normalization used in notebooks: replace underscores, then encoder normalize. + normalized = [self._normalize_subcategory(sub) for sub in self.subcategories] + vectors = self.encoder.batch_encode( + normalized, + encoder_type="response_encoder", + batch_size=self.batch_size, + ) + + np.savez( + self.embeddings_path, + vectors=vectors, + subcategories=self.subcategories, + ) + logger.info(f"Saved response embeddings to {self.embeddings_path}") + + return vectors + + def _normalize_subcategory(self, name: str) -> str: + """ + Normalize subcategory strings for stable embeddings. + + Args: + name (str): Subcategory name. + + Returns: + str: Normalized subcategory name. + """ + + name = name.replace("_", " ") + + return self.encoder.normalize_text(name) + + def _combine_scores( + self, sim_scores: np.ndarray, bm25_scores: np.ndarray + ) -> np.ndarray: + """ + Combine similarity and BM25 scores. + + Args: + sim_scores (np.ndarray): Similarity scores. + bm25_scores (np.ndarray): BM25 scores. + + Returns: + np.ndarray: Combined scores. + """ + if self.bm25_weight <= 0: + return sim_scores + + bm25_max = bm25_scores.max() if bm25_scores.size else 0.0 + sim_max = sim_scores.max() if sim_scores.size else 0.0 + + bm25_norm = ( + bm25_scores / (bm25_max + 1e-9) + if bm25_max > 0 + else np.zeros_like(bm25_scores) + ) + + sim_norm = ( + sim_scores / (sim_max + 1e-9) if sim_max > 0 else np.zeros_like(sim_scores) + ) + + return self.bm25_weight * bm25_norm + (1 - self.bm25_weight) * sim_norm + + def retrieve(self, text: str, top_k: int = 10) -> list[str]: + """ + Retrieve top-k subcategories for a given text. + + Args: + text (str): Input text. + top_k (int, optional): Number of top subcategories to retrieve. Defaults to 10. + + Returns: + list[str]: List of top-k subcategories. + """ + return self.batch_retrieve([text], top_k=top_k)[0] + + def batch_retrieve(self, texts: Iterable[str], top_k: int = 10) -> list[list[str]]: + """ + Batch retrieve top-k subcategories for given texts. + + Args: + texts (Iterable[str]): Input texts. + top_k (int, optional): Number of top subcategories to retrieve. Defaults to 10. + + Returns: + list[list[str]]: List of lists of top-k subcategories for each input text. + """ + texts = list(texts) + if not texts: + return [] + + query_vectors = self.encoder.batch_encode( + texts, encoder_type="question_encoder", batch_size=self.batch_size + ) + similarity = np.inner(query_vectors, self.response_vectors) + k = min(top_k, similarity.shape[1]) + + results = [] + for idx, text in enumerate(texts): + sim_scores = similarity[idx] + + if self.bm25 and self.bm25_weight > 0: + bm25_scores = self.bm25.score_vector(text) + combined = self._combine_scores(sim_scores, bm25_scores) + else: + combined = sim_scores + + top_indices = np.argsort(-combined)[:k] + results.append([self.subcategories[i] for i in top_indices]) + + return results + + def __call__(self, item: DataItem) -> DataItem: + """ + Apply subcategorization to entities in the data item. + + Args: + item (DataItem): Input data item. + + Returns: + DataItem: Data item with added subclass labels. + """ + item = deepcopy(item) + ents = get_element(item, levels=["predictions", "entities"]) or [] + texts = [ent["text"] for ent in ents] + filtered_ents = filter_by_category(ents, self.category) + retrieved = self.batch_retrieve(texts, top_k=5) + + for ent, retrieved_ in zip(ents, retrieved): + if ent in filtered_ents and not get_element( + ent, levels=["attrs", "aymurai_label_subclass"] + ): + ent.setdefault("attrs", {})["aymurai_label_subclass"] = retrieved_ + + item.setdefault("predictions", {})["entities"] = ents + + return item diff --git a/aymurai/transforms/entity_subcategories/subcategories.py b/aymurai/transforms/entity_subcategories/subcategories.py new file mode 100644 index 00000000..ad4fd5a9 --- /dev/null +++ b/aymurai/transforms/entity_subcategories/subcategories.py @@ -0,0 +1,638 @@ +SUBCATEGORIES = { + "conducta": [ + "abandonar_animal_domestico", + "abandono_de_personas", + "abuso_de_armas", + "abuso_de_autoridad_e_incumplimiento_deberes_funcionario_publico", + "abuso_sexual", + "acceder_a_lugares_distintos_segun_entrada", + "acceder_lugares_distintos_segun_entrada", + "acceso_a_sistema_restringido", + "acoso_sexual_callejero", + "actividades_lucrativas_sin_autorizacion", + "actividades_lucrativas_sin_habilitacion", + "actos_contenido_sexual_con_menores", + "administracion_fraudulenta", + "afectar_desarrollo_espectaculo", + "afectar_el_desarrollo_del_espectaculo", + "afectar_funcionamiento_servicios_publicos", + "afectar_servicios_de_emergencia_o_seguridad", + "afectar_servicios_emergencia", + "afectar_señalizacion", + "allanamiento_autonomo", + "alterar_programa", + "alterar_sepulturas", + "amenazas", + "amparo", + "apariencia_falsa", + "apariencia_falsa_para_entrar_a_domicilio_o_lugar_privado", + "apropiacion_indebida_de_tributos", + "arrojar_cosas_que_puedan_causar_lesiones", + "arrojar_cosas_sustancias", + "arrojar_sustancias_insalubres_en_lugares_publicos", + "asociacion_ilicita", + "asuncion_falsa_de_contravencion", + "atentado_contra_la_autoridad", + "ausencia_de_habilitacion", + "banderas", + "carteles_afiches_volantes", + "cierre_defectuoso", + "circular_por_carriles_exclusivos", + "circular_por_lugar_prohibido", + "cohecho_activo", + "cohecho_pasivo", + "coimas", + "competencia_desleal", + "conducir_con_mayor_cantidad_de_alcohol_en_sangre_del_permitido", + "conducir_con_mayor_grado_de_alcohol_o_bajo_efectos_de_estupefacientes", + "conducir_sin_licencia_que_lo_habilite_por_categoria_de_vehiculo", + "conducir_usando_aparato_electronico", + "connivencia_policial", + "contactar_menor_por_medio_de_tecnologias_para_cometer_delitos_contra_su_integridad_sexual", + "cruce_de_semaforo_en_rojo", + "cuida_coche", + "cuidar_coches_sin_autorizacion", + "daños", + "daños_informaticos", + "defraudacion", + "defraudacion_contra_la_administracion_publica", + "delito_contra_seguridad_transito", + "denegacion_de_justicia", + "derecho_admision", + "desarmado_automotor", + "desinfeccion_y_desratizacion", + "desobediencia_a_cargas_procesales", + "desobediencia_a_la_autoridad", + "difusion_no_autorizada_de_imagenes", + "difusion_no_autorizada_de_imagenes_intimas", + "discriminar", + "documentacion_sanitaria", + "ejecucion_de_multa", + "ejercer_ilegitimamente_actividad", + "ejercicio_ilegal_de_la_medicina", + "elementos_de_prevencion_contra_incendio", + "encubrimiento", + "encubrimiento_actividades_baile", + "ensuciar_bienes", + "entregar_indebidamente_armas_explosivos", + "espantar_animales", + "espejos_retrovisores", + "estacionamiento_prohibido", + "estacionamiento_sobre_senda_peatonal", + "estafa", + "estupefacientes", + "exceso_capacidad_ingreso", + "exceso_de_velocidad", + "exhibicion_de_documentacion_obligatoria", + "exhibiciones_obscenas", + "exhortos_de_otras_jurisdicciones", + "explotacion_trabajo_infantil", + "extorsion", + "fabricar_transportar_artefactos_pirotecnicos", + "falsa_denuncia", + "falsedad_documental_de_certificado_de_aptitud_ambiental", + "falsificacion_certificado_medico", + "falsificacion_de_documento", + "falsificacion_de_marcas_firmas_señas_oficiales", + "falsificacion_de_numeracion_de_objeto_registrada_de_acuerdo_a_ley", + "falta_de_poliza_de_seguros", + "favorecimiento_evasion_persona_detenida", + "frustrar_subasta_publica", + "guardar_artefactos_pirotecnicos", + "guardar_elementos_aptos_violencia", + "habeas_corpus", + "habilitacion_en_infraccion", + "homicidio", + "hostigamiento", + "hostigamiento_digital", + "hurto", + "impedimento_de_contacto_de_menor_con_padre_no_conviviente", + "incendio_explosion_inundacion_con_peligro_para_bienes", + "incendios_y_otros_estragos", + "incitar_al_desorden", + "incitar_desorden", + "incumplimiento_de_plazos", + "incumplimiento_deberes_familiares", + "incumplimiento_deberes_funcionario_publico", + "incumplimiento_medidas_prevencion_sanidad", + "incumplimiento_perimetro_profundidad", + "incumplir_clausura", + "incumplir_obligaciones_legales", + "inducir_menor_a_mendigar", + "infraccion_reglamentos_seguridad", + "ingresar_artefactos_pirotecnicos", + "ingresar_consumir_bebidas_alcoholicas", + "ingresar_contra_derecho_admision", + "ingresar_sin_autorizacion_lugares_reservados", + "ingresar_sin_entrada", + "ingresar_sin_entrada_autorizacion", + "ingreso_a_domicilio_sin_autorizacion", + "inhumar_exhumar_profanar", + "intimidacion", + "juego_sin_autorizacion", + "juegos_de_azar_sin_autorizacion", + "lesiones", + "ley_451", + "licencia_vencida", + "maltrato", + "mantener_animal_domestico_espacios_inadecuados", + "mantenimiento_de_cercas_y_aceras", + "menoscabar_integridad_animal_domestico", + "mesnna_masnna", + "no_realizar_grabado_de_autopartes", + "no_respetar_carriles", + "no_respetar_senda_peatonal", + "obligacion_de_exhibir_cartel", + "obligacion_de_informar_prohibicion_de_fumar", + "obligacion_de_conservar", + "obstaculizar_ingreso_o_salida", + "obstruccion_de_inspeccion", + "obstruccion_de_via", + "obstruccion_de_via_publica", + "obstruccion_via_publica", + "obstruir_salida", + "ocupar_via_publica", + "oferta_demanda_sexo_espacio_publico", + "omitir_cuidados_animal_domestico", + "omitir_recaudos_de_cuidado_animal_domestico", + "omitir_recaudos_de_organizacion", + "omitir_recaudos_espectaculo_masivo", + "omitir_recaudos_organizacion_seguridad", + "organizar_explotar_juego", + "organizar_sin_autorizacion_juego", + "participar_competencias_velocidad_via_publica", + "pelear_en_lugar_publico", + "permiso_y_planos_de_obra", + "persona_no_habilitada", + "perturbar_ceremonias_religiosas_funebres", + "perturbar_filas_ingreso_no_respetar_vallado", + "placas_de_dominio", + "pornografia_infantil", + "portacion_de_arma", + "portar_armas_no_convencionales", + "presencia_menores_lugar_no_autorizado", + "presunta_comision_delito", + "presunta_contravencion", + "privacion_ilegitima_de_la_libertad", + "producir_avalanchas", + "promocion_o_facilitacion_de_prostitucion_de_personas", + "promover_comerciar_ofertar_juego", + "propaganda_discriminatoria", + "proteccion_animal", + "provocar_parcialidad", + "publicidad_en_lugares_no_habilitados_via_publica", + "regentear_o_administrar_casas_de_tolerancia", + "residuos_peligrosos", + "retencion_indebida", + "revender_entradas", + "robo", + "ruidos_molestos", + "ruidos_y_vibraciones", + "sancion_generica", + "servicios_de_seguridad_prohibiciones_incumplidas", + "servicios_de_seguridad_requisitos_incumplidos", + "suministrar_alcohol_menor", + "suministrar_bebidas_alcoholicas", + "suministrar_elementos_aptos_agresion", + "suministrar_material_pornografico", + "suministrar_objetos_peligrosos", + "suministrar_productos_farmaceuticos", + "suministro_de_medicamentos", + "suplantacion_identidad", + "suplantacion_digital_de_identidad", + "taxis_transportes_escolares_remises_sin_autorizacion", + "tenencia_de_arma", + "transporte_de_pasajeros", + "transporte_de_pasajeros_sin_habilitacion", + "transporte_de_pasajeros_sin_habilitacion_o_autorizacion", + "usar_indebidamente_credencial", + "usar_indebidamente_espacio_publico", + "usar_indebidamente_armas", + "uso_de_documento_falso_o_adulterado", + "uso_indebido_del_espacio_publico", + "uso_o_exhibicion_de_franquicias", + "usurpacion", + "vehiculo_abandonado", + "vender_entradas_o_permitir_ingreso_exceso", + "vender_sustancias_medicinales_sin_receta", + "venta_o_consumo_de_bebidas_alcoholicas_fuera_del_horario", + "verificacion_tecnica", + "violacion_de_peaje", + "violacion_de_domicilio", + "violacion_de_secretos_y_de_la_privacidad", + "violar_clausura", + "violar_inhabilitacion_para_conducir", + "violar_reglamentacion_juego", + "zanjas_y_pozos_en_via_publica", + ], + "conducta_descripcion": [ + "agravada", + "agravadas_por_edad", + "agravadas_por_el_uso_de_armas", + "agravadas_por_uso_de_armas", + "agravadas_por_uso_de_armas_impropia", + "agravado", + "agravado_abuso_funcion", + "agravado_alevosia", + "agravado_causar_sufrimiento_otra_persona", + "agravado_concurso", + "agravado_familiar", + "agravado_insertar_datos_en_datos_personales", + "agravado_lugar_publico", + "agravado_mas_personas", + "agravado_medio_idoneo", + "agravado_menor_de_edad", + "agravado_miembro_fuerzas", + "agravado_monumentos", + "agravado_otro_delito", + "agravado_peligro_de_muerte", + "agravado_por_el_vinculo", + "agravado_por_espacio", + "agravado_por_ser_fuerza_de_seguridad", + "agravado_precio", + "agravado_relacion_de_pareja", + "agravado_superior_militar", + "agravado_violar_informacion_banco_de_datos", + "agravado_violar_sistemas_seguridad", + "agravado_violencia_de_genero", + "agravado_violencia_mujer", + "agravados", + "coactivas", + "coactivas_agravadas", + "coactivas_agravadas_por_uso_de_armas", + "comercio_de_plantas_para_producir_estupefacientes", + "con_escalamiento", + "conduccion_imprudente", + "culposas", + "culposo", + "de_fuego_uso_civil", + "de_guerra", + "destinado_a_acreditar_identidad_de_personas_habilitacion_o_titularidad", + "digital", + "digital_agravado_familiar", + "digital_agravado_jefe", + "digital_agravado_mas_de_una_persona", + "digital_agravado_menor_de_edad", + "digital_agravado_relacion_de_pareja", + "durante_espectaculo_deportivo", + "en_grandes_parques_o_espectaculos_masivos", + "en_riña", + "entrega_suministro_facilitacion", + "funcionario_publico", + "ganzua_llave", + "graves", + "graves_tentativa", + "informatica", + "ingreso_a_domicilio_sin_autorizacion", + "intimadacion_o_invocando_orden_superior", + "leves", + "leves_agravadas", + "muebles_transportables", + "no_denuncia_delito", + "ocasion_de_incendio", + "organismo_publico", + "para_produccion_o_tenencia_con_fines_de_comercializacion", + "por_despojo", + "por_obligacion_de_devolver", + "por_turbacion_de_la_posesion", + "productos_separados_suelo", + "publico_o_privado", + "seguido_de_muerte", + "simple", + "simples", + "sin_autorizacion", + "tarjeta_credito_debito", + "tenencia", + "tentativa", + "tentativa_agravado_alevosia", + "transporte_de_pasajeros_sin_habilitacion_o_autorizacion", + "uso_de_tarjeta_o_datos", + "vehiculos", + ], + "detalle": [ + "audiencia_victima_ley26485", + "30_dias", + "45_dias", + "60_dias", + "90_dias", + "180_dias", + "abandono_vehiculo", + "absolucion", + "absolucion_desistimiento_fiscal", + "accesorias_legales", + "acepta_competencia_inhibitoria", + "acepta_cuestion_turno", + "aclaratoria", + "acta_de_comprobacion", + "acta_de_intimacion", + "acuerdo_de_pago_levanta_embargo", + "acumulacion_por_conexidad_y_ampliacion", + "admisibilidad", + "agente_encubierto", + "agente_revelador", + "agotamiento_de_la_pena", + "alimentos_provisorios", + "allanamiento", + "apelacion", + "apertura_extraccion_examen_datos", + "aprehension", + "arma", + "armas", + "arresto_domiciliario_deja_sin_efecto", + "asesor_tutelar", + "atipicidad_sobreseimiento", + "audiencia_acusado_ley26485", + "audiencia_acusado_difiere_sentencia", + "audiencia_preliminar", + "audiencia_victima_11bis_ley24660", + "bloqueo_acceso_al_dominio", + "cadena_de_custodia_de_elementos_secuestrados", + "camara_gesell", + "cambio_domicilio_prision_domiciliaria", + "captura", + "captura_deja_sin_efecto", + "caucion_real", + "cedula_de_notificacion", + "cese_de_actos_de_perturbacion_o_intimidacion", + "cese_de_medidas", + "cese_de_medidas_parcial", + "cierre_de_instancia", + "clausura", + "clausura_deja_sin_efecto", + "competencia_penal_juvenil", + "concede", + "concede_parcialmente", + "conocimiento_personal", + "consigna_policial", + "contienda_negativa", + "control", + "convalida_fallecimiento", + "convalida_inimputabilidad", + "convalidacion_otras_causales", + "copia_forense_celular", + "copias_de_capturas_de_pantalla_mensajes_y_llamados", + "cosa_juzgada_sobreseimiento", + "cuarto_intermedio", + "cuarto_intermedio_para_resolver", + "cumplimiento_de_regla_de_conducta_en_complejo_penitenciario", + "cumplimiento_de_reglas_de_conducta", + "declaracion_testimonial", + "declina_competencia", + "defensa", + "defensa_particular", + "defensa_querella", + "deja_sin_efecto_inhabilitacion_y_devuelve_licencia", + "deja_sin_efecto_revocacion", + "delitos_de_accion_publica", + "desistimiento_accion_instancia_privada_sobreseimiento", + "desistimiento_fiscal", + "destruccion_de_arma", + "detencion", + "detencion_captura_y/o_traslado", + "detencion_captura_y/o_traslado_deja_sin_efecto", + "detencion_secuestro_y_requisa", + "devolucion_acarreo_y_estadia", + "dictamen_fiscal", + "difiere_decision", + "difiere_fundamentos", + "difiere_resolucion", + "dispositivo_de_geoposicionamiento_mantiene_hasta_juicio", + "domiciliaria_con_vigilancia_electronica", + "egreso_del_pais", + "ejecucion_de_honorarios", + "embargo", + "embargo_deja_sin_efecto", + "estimulo_educativo", + "estupefacientes", + "estupefacientes_dinero", + "estupefacientes_telefono_dinero", + "estupefacientes_telefono_dinero_arma", + "estupefacientes_telefono_vehiculo", + "estupefacientes_vehiculo_dinero", + "excarcelacion", + "exclusion_del_hogar", + "exhorto", + "exhorto_diplomatico", + "exime_pago_multa", + "exime_reparacion_del_daño_y_pago_multa", + "eximicion_pago", + "eximicion_pauta_de_conducta", + "expulsion_art_64_a_ley_25871", + "extincion_accion_por_autocomposicion", + "extincion_accion_por_cumplimiento", + "extincion_accion_sobreseimiento", + "extincion_accion_sobreseimiento_por_fallecimiento", + "extincion_de_la_pena", + "extincion_sancion_por_cumplimiento", + "extrañamiento", + "falta_de_accion_sobreseimiento", + "falta_de_legitimacion_pasiva", + "falta_de_participacion", + "falta_de_presentacion", + "falta_de_requisitos_procedencia", + "fija_nueva_audiencia", + "fondo_de_reserva", + "habeas_corpus", + "hasta_agotar_investigacion", + "identidad_objeto_y_sujeto", + "imagenes_videos", + "incompetencia_material", + "incompetencia_por_conexidad", + "incompetencia_por_conexidad_subjetiva", + "incompetencia_territorial", + "indeterminacion_imputacion", + "informacion_y_apertura_de_antenas_llamadas", + "informe_cij", + "informe_mercado_libre", + "informe_ncmec", + "informe_previo", + "informes_llamadas_y_deteccion_de_celdas", + "inhabilitacion_para_conducir", + "inhabilitacion_para_conducir_deja_sin_efecto", + "inmovilizacion", + "interes_en_el_caso", + "internacion_involuntaria", + "interprete", + "interprete_y_defensa", + "interrogatorio_policial", + "intervencion_equipo_especializado", + "intervencion_previa", + "intervencion_red_social", + "intervencion_telefonica", + "intervencion_telefonica_con_ubicacion_de_celda_de_apertura", + "levantamiento_clausura_administrativa", + "levantamiento_secreto_bancario", + "levantamiento_secreto_fiscal", + "liberacion_aves", + "libertad_asistida_incorporacion", + "libertad_condicional_otorga", + "libertad_condicional_revoca", + "litispendencia", + "mantiene", + "mediacion", + "medidas_restrictivas", + "modifica_pauta_de_conducta", + "morigeracion_domiciliaria", + "no_convalida", + "no_homologa_absolucion", + "notificacion_via_mail", + "nulidad_incorporacion_prueba", + "otorga_plazo", + "otras_vias_idoneas", + "pago_de_multa_en_cuotas", + "pago_minimo_multa_sobreseimiento", + "pago_minimo_multa_y_reparacion_daño_64cp", + "paradero", + "paradero_deja_sin_efecto", + "parcial_requerimiento_de_juicio", + "parcial_requerimiento_de_juicio_falta_de_circunscripcion_temporal", + "parcial_requerimiento_de_juicio_falta_de_fundamentacion", + "parcial_requerimiento_de_juicio_por_introduccion_de_agravante", + "pedido_de_informe_banco", + "pedido_de_informe_facebook", + "pedido_de_informe_facebook_google", + "pedido_de_informe_facebook_microsoft", + "pedido_de_informe_google", + "pedido_de_informe_google_microsoft", + "pedido_de_informe_imgur_llc", + "pedido_de_informe_instagram", + "pedido_de_informe_microsoft", + "pedido_de_informe_skype", + "pedido_de_informe_telefonia", + "pedido_de_informe_whatsapp", + "pedido_informe_tiktok", + "pedido_prision_domiciliaria", + "pena_de_arresto_efectivo_cumplimiento", + "pena_de_arresto_efectivo_cumplimiento_compurgada", + "pena_de_arresto_en_suspenso", + "pena_de_inhabilitacion_especial", + "pena_de_multa_efectivo_cumplimiento", + "pena_de_multa_en_suspenso", + "pena_de_multa_sustituida_por_amonestacion", + "pena_de_multa_sustituida_por_clausura_se_tiene_por_cumplida", + "pena_de_multa_sustituida_por_trabajos_comunitarios", + "pena_de_prision_domiciliaria", + "pena_de_prision_efectivo_cumplimiento", + "pena_de_prision_efectivo_cumplimiento_compurgada", + "pena_de_prision_efectivo_cumplimiento_domiciliaria", + "pena_de_prision_en_suspenso", + "pena_de_tareas_comunitarias", + "pena_de_tareas_de_utilidad_publica", + "pericia", + "pericia_arma", + "pericia_examen_veterinario", + "peritaje_de_telefono", + "peritaje_ordenadores_y_o_telefonos", + "peritaje_telefono", + "perito", + "permiso_de_viaje", + "plazo_razonable_sobreseimiento", + "plazo_razonable_sobresimiento", + "pleito_con_una_parte", + "por_quince_dias_hasta_tomar_contacto_con_victima", + "prescripcion_sobreseimiento", + "prision_domiciliaria_mantiene", + "prision_domiciliaria_prorroga", + "procedimiento", + "procedimiento_administrativo_pendiente", + "prohibicion_compra_tenencia_registracion_de_armas", + "prohibicion_de_acercamiento", + "prohibicion_de_acercamiento_y_contacto", + "prohibicion_de_publicar_en_redes", + "prohibicion_de_publicar_en_redes_sobre", + "promocion_a_periodo_de_prueba_art_7_ley_24660", + "prorroga", + "prueba_ofrecida", + "publicacion_de_edictos", + "queja", + "querella", + "quita_puntos_de_licencia_de_conducir", + "realizacion", + "rebeldia", + "rebeldia_deja_sin_efecto", + "rebeldia_y_captura", + "rebeldia_y_paradero", + "rechaza_acuerdo", + "rechaza_competencia", + "regimen_de_comunicacion_provisoria", + "reincidencia", + "reintegra_puntos_de_licencia_de_conducir", + "remate_vehiculo", + "remision_a_justicia_civil", + "remision_camara_para_sorteo", + "reparacion_integral_del_daño_sobreseimiento", + "replica_de_arma_de_plastico", + "reposicion", + "reposicion_apelacion_subsidio", + "reprogramacion", + "requerido_por_infractor", + "requerimiento_de_juicio", + "requerimiento_de_juicio_falta_de_fundamentacion", + "requisa", + "restitucion_de_inmueble", + "revision_psicofisica_capacidad", + "revision_psiquiatrica_y_psicologica", + "revoca", + "revoca_condicionalidad", + "revocacion", + "rueda_de_reconocimiento", + "salidas_extraordinarias", + "salidas_transitorias", + "sancion_sustitutiva_prorroga", + "secuestro", + "sin_indicacion_plazo", + "sobreseimiento", + "solicita", + "solicitud_destruccion_de_estupefacientes", + "tareas_de_investigacion", + "telefono", + "temor_parcialidad_generico", + "tiene_por_desistido", + "tiene_por_no_computado_plazo", + "tiene_presente", + "tiene_presente_acuerdo", + "tobillera_refractaria", + "traslado_por_fuerza_publica", + "unifica_solicita_inhibitoria", + "unificacion_de_pena", + "vehiculo", + "vencimiento_plazo_ipp", + ], + "objeto_de_la_resolucion": [ + "acumulacion_de_caso", + "admisibilidad_amparo", + "admisibilidad_habeas_corpus", + "admisibilidad_prueba", + "apartamiento", + "archivo_fiscal", + "audiencia_de_conciliacion", + "beneficio_litigar_sin_gastos", + "caducidad_de_instancia", + "cuestion_de_competencia", + "decomiso", + "desistimiento_faltas", + "ejecucion_de_la_pena", + "excepcion", + "excusacion", + "extraccion_testimonios", + "juicio_abreviado", + "juicio_oral", + "juzgamiento_faltas", + "mediacion", + "medida_cautelar", + "medida_probatoria", + "mesa_de_dialogo", + "nulidad", + "prision_preventiva", + "prorroga_investigacion_penal_preparatoria", + "recurso", + "recusacion", + "regulacion_de_honorarios", + "remision_controladora", + "restablecimiento_de_contacto", + "restitucion_de_efectos", + "restitucion_de_inmueble", + "restitucion_efectos", + "suspension_del_proceso_a_prueba", + ], +} diff --git a/aymurai/transforms/entity_subcategories/usem.py b/aymurai/transforms/entity_subcategories/usem.py deleted file mode 100644 index c70df207..00000000 --- a/aymurai/transforms/entity_subcategories/usem.py +++ /dev/null @@ -1,123 +0,0 @@ -import os -from hashlib import md5 -from copy import deepcopy - -import numpy as np -import tensorflow as tf - -from aymurai.logger import get_logger -from aymurai.meta.types import DataItem -from aymurai.models.usem.core import USEMQA -from aymurai.utils.download import download -from aymurai.utils.misc import is_url, get_element -from aymurai.meta.pipeline_interfaces import Transform - -from .utils import filter_by_category - -logger = get_logger(__name__) - - -class USEMSubcategorizer(Transform): - """ - Use USEM to retrieve subcategories - """ - - usem = USEMQA() - - def __init__( - self, - category: str, - subcategories_path: list[str], - response_embeddings_path: str, - device: str = "/cpu:0", - ): - """ - Sentence Similarity using USEM (Universal Sentence Encoder Multilingual QA) - - Args: - category (str): category to filter - subcategories_path (list[str]): path to subcategories - response_embeddings_path (str): path to response embeddings - device (str, optional): device to use. Defaults to "/cpu:0". - """ - self.category = category - - CACHE_PATH = os.path.join( - os.getenv("AYMURAI_CACHE_BASEPATH", "/resources/cache/aymurai"), - self.__name__, - ) - - logger.info(f"load usem options from {subcategories_path}") - if is_url(url := subcategories_path): - fname = md5(url.encode("utf-8")).hexdigest() - subcategories_path = f"{CACHE_PATH}/{fname}" - logger.info(f"downloading options on {subcategories_path}") - os.makedirs(CACHE_PATH, exist_ok=True) - subcategories_path = download(url, output=subcategories_path) - - with open(subcategories_path, "r") as file: - self.subcategories = file.read().splitlines() - logger.info(f"options head: {self.subcategories[:5]}") - - # download embeddings - if is_url(url := response_embeddings_path): - fname = md5(url.encode("utf-8")).hexdigest() - model_path = f"{CACHE_PATH}/{fname}" - logger.info(f"downloading embeddings on {model_path}") - os.makedirs(CACHE_PATH, exist_ok=True) - response_embeddings_path = download(url, output=model_path) - logger.info(f"load usem embeddings from {response_embeddings_path}") - self.usem_vectors = self.load_usem_vectors(response_embeddings_path) - self.device = device - - def load_usem_vectors(self, file_path): - """ - Load USEM vectors - """ - usem_vectors = np.load(file_path) - return usem_vectors.copy() - - def retrieve(self, text: str, top_k: int = 10) -> list[str]: - """ - Retrieve similar sentences using USEM - """ - with tf.device(self.device): - query_vector = self.usem.encode( - [text], - encoder_type="question_encoder", - ) - - products = np.inner(query_vector, self.usem_vectors)[0] - similar_idx = np.flip(products.argsort())[:top_k] - similar_sentences = [self.subcategories[idx] for idx in similar_idx] - - return similar_sentences - - def __call__(self, item: DataItem) -> DataItem: - """ - Retrieve subcategories for entities. If the entity is not in the category of interest, - the subcategory is not retrieved. - - Args: - item (DataItem): input data item - - Returns: - DataItem: output data item - """ - item = deepcopy(item) - - ents = get_element(item, levels=["predictions", "entities"]) or [] - - texts = list(map(lambda x: x["text"], ents)) - filtered_ents = filter_by_category(ents, self.category) - retrieved = list(map(self.retrieve, texts)) - - for ent, retrieved_ in zip(ents, retrieved): - if ent in filtered_ents and not get_element( - ent, levels=["attrs", "aymurai_label_subclass"] - ): - ent["attrs"]["aymurai_label_subclass"] = retrieved_ - - item["predictions"]["entities"] = ents - - return item diff --git a/aymurai/utils/alignment/core.py b/aymurai/utils/alignment/core.py index c8eeeed2..65a3004c 100644 --- a/aymurai/utils/alignment/core.py +++ b/aymurai/utils/alignment/core.py @@ -11,6 +11,15 @@ def tokenize(text: str) -> list[str]: + """ + Split multi-line text into whitespace-delimited tokens. + + Args: + text (str): Input text to tokenize. + + Returns: + list[str]: Tokens extracted from the text. + """ tokens = map(str.split, text.splitlines()) tokens = flatten(tokens) return list(tokens) @@ -21,15 +30,17 @@ def align_text( target_text: str, columns: tuple[str, str] = ("source", "target"), ) -> pd.DataFrame: - """align source and target text into a table + """ + Align source and target text into a token-level mapping table. Args: - source_text (str): reference text - target_text (str): second text to align with - columns (tuple[str, str]): names of columns on output + source_text (str): Reference text. + target_text (str): Text to align against `source_text`. + columns (tuple[str, str], optional): Output column names. + Defaults to ("source", "target"). Returns: - pd.DataFrame: alignment table + pd.DataFrame: Alignment table. """ source_tokens = [t.strip() for t in tokenize(source_text)] target_tokens = [t.strip() for t in tokenize(target_text)] @@ -78,15 +89,22 @@ def align_docs( columns: tuple[str, str] = ("source", "target"), source_preprocess: Callable[[str], str] | None = None, target_preprocess: Callable[[str], str] | None = None, -): - """align two documents word to word +) -> pd.DataFrame: + """ + Align two documents word by word. Args: - source_path (str | Path): source document path (reference) - target_path (str | Path): target document path (target) + source_path (str | Path): Source document path (reference). + target_path (str | Path): Target document path. + columns (tuple[str, str], optional): Output column names. + Defaults to ("source", "target"). + source_preprocess (Callable[[str], str] | None, optional): Preprocessing + function applied to extracted source text. Defaults to None. + target_preprocess (Callable[[str], str] | None, optional): Preprocessing + function applied to extracted target text. Defaults to None. Returns: - pd.DataFrame: alignment table + pd.DataFrame: Alignment table. """ source: str = extract_document(source_path, errors="raise") # type: ignore target: str = extract_document(target_path, errors="raise") # type: ignore @@ -140,6 +158,16 @@ def add_empty_lines_between_paragraphs( reference: str, mapping: pd.DataFrame, ) -> pd.DataFrame: + """ + Insert empty rows in an alignment table at paragraph break boundaries. + + Args: + reference (str): Reference text used to infer paragraph boundaries. + mapping (pd.DataFrame): Alignment table to augment. + + Returns: + pd.DataFrame: Alignment table with inserted empty rows. + """ mapping = mapping.copy() reference = re.sub(r"\n+", "\n", reference) splitted = reference.splitlines() diff --git a/aymurai/utils/alignment/ia2.py b/aymurai/utils/alignment/ia2.py index f94af1a3..68f7755a 100644 --- a/aymurai/utils/alignment/ia2.py +++ b/aymurai/utils/alignment/ia2.py @@ -1,5 +1,6 @@ import os import re +from typing import Any import numpy as np import pandas as pd @@ -7,12 +8,31 @@ def normalize(text: str) -> str: + """ + Normalize text by removing accents and non-word characters. + + Args: + text (str): Input text. + + Returns: + str: Normalized text. + """ text = unidecode(text) text = re.sub(r"\W", "", text) return text -def norm_ia2_label(text, labels: list[str]) -> str | float: +def norm_ia2_label(text: Any, labels: list[str]) -> str | float: + """ + Extract a single matching IA2 label from text. + + Args: + text (Any): Input text containing IA2 label content. + labels (list[str]): Valid label patterns to match. + + Returns: + str | float: The matched label when unique, otherwise `np.nan`. + """ if not isinstance(text, str): return np.nan @@ -27,6 +47,15 @@ def norm_ia2_label(text, labels: list[str]) -> str | float: def label_to_conll_format(labels: pd.Series) -> pd.Series: + """ + Convert label sequences to CONLL BIO tags. + + Args: + labels (pd.Series): Sequence of labels. + + Returns: + pd.Series: Labels converted to BIO format. + """ labels = labels.copy() if len(labels.dropna()) == 0: @@ -43,6 +72,15 @@ def label_to_conll_format(labels: pd.Series) -> pd.Series: def ia2_text_preprocess(text: str) -> str: + """ + Insert whitespace after IA2 XML-like tags when missing. + + Args: + text (str): Input text. + + Returns: + str: Preprocessed text. + """ text = re.sub(r"(<\w+>)(\S)", r"\g<1> \g<2>", text) return text @@ -52,7 +90,18 @@ def mapping2conll( filename: str, text_column: str = "original", label_column: str = "label", -): +) -> None: + """ + Write token-label mappings to a CONLL-style file. + + Args: + df (pd.DataFrame): DataFrame containing token and label columns. + filename (str): Output file path. + text_column (str, optional): Column name containing tokens. + Defaults to "original". + label_column (str, optional): Column name containing labels. + Defaults to "label". + """ dir = os.path.dirname(filename) os.makedirs(dir, exist_ok=True) with open(filename, "w") as file: diff --git a/aymurai/utils/cache.py b/aymurai/utils/cache.py index 48a4ae75..6d1928eb 100644 --- a/aymurai/utils/cache.py +++ b/aymurai/utils/cache.py @@ -1,13 +1,12 @@ -import os import json +import os import pickle -from typing import Any, Optional +from typing import Any -import joblib import diskcache +import joblib from aymurai.logger import get_logger -from aymurai.meta.types import DataItem from aymurai.utils.json_encoding import EnhancedJSONEncoder logger = get_logger(__name__) @@ -18,15 +17,15 @@ def flatten_dict(current: dict, key: str = "", result: dict = {}) -> dict: """ - Flatten a dict + Flatten nested dictionaries into a dotted-key mapping. Args: - current (dict): dict to be flattened - key (str, optional): key to be used. Defaults to "". - result (dict, optional): result dict. Defaults to {}. + current (dict): Source dictionary to flatten. + key (str, optional): Parent key prefix. Defaults to "". + result (dict, optional): Accumulator reused across recursion. Defaults to {}. Returns: - dict: flattened dict + dict: Mapping of flattened keys to terminal values. """ if type(current) is dict: for k in current: @@ -37,12 +36,12 @@ def flatten_dict(current: dict, key: str = "", result: dict = {}) -> dict: return result -def cache_clear(keys: list[str]): +def cache_clear(keys: list[str]) -> None: """ - Clear cache + Remove the provided keys from the disk-backed cache. Args: - keys (list[str]): keys to be cleared + keys (list[str]): Cache keys to delete. """ for key in keys: cache.pop(key) @@ -50,14 +49,15 @@ def cache_clear(keys: list[str]): def get_cache_key(item: Any, context: Any = "") -> str: """ - Get cache key + Build a stable cache key combining the item payload and context. Args: - item (Any): Data to hash - context (Any): context object to create hash + item (Any): Value to hash into the key namespace. + context (Any, optional): Additional context used to scope the key. + Defaults to "". Returns: - str: hash + str: Deterministic hash suitable for cache lookups. """ if type(item) in [dict]: @@ -72,20 +72,26 @@ def get_cache_key(item: Any, context: Any = "") -> str: return cache_key -def is_cached(key: str): +def is_cached(key: str) -> bool: + """ + Determine whether a cache entry exists for the given key. + + Args: + key (str): Cache key to inspect. + + Returns: + bool: True when the key is present, otherwise False. + """ return key in cache -def cache_save( - data_item: DataItem, - key: str, -): +def cache_save(data_item: Any, key: str) -> None: """ - save data on cache + Persist a Python object in the disk-backed cache. Args: - data (Data): data to be cached - key (str): key to store + data_item (Any): Serializable payload to store. + key (str): Cache key under which the payload is saved. """ data = pickle.dumps(data_item) @@ -94,18 +100,16 @@ def cache_save( cache.set(key, data) -def cache_load(key: str) -> Optional[DataItem]: +def cache_load(key: str) -> Any | None: """ - load data from cache + Retrieve and deserialize a cached payload when present. Args: - key (str): key to load + key (str): Cache key to resolve. Returns: - Optional[Data]: loaded data - + Any | None: Cached payload on hit, otherwise ``None``. """ - if key in cache: # Retrieve the serialized object from cache data = cache.get(key) diff --git a/aymurai/utils/display/pandas.py b/aymurai/utils/display/pandas.py index 2f83742b..d2abc054 100644 --- a/aymurai/utils/display/pandas.py +++ b/aymurai/utils/display/pandas.py @@ -1,7 +1,18 @@ +from typing import ContextManager + import pandas as pd from more_itertools import flatten -def pandas_context(**kwargs): +def pandas_context(**kwargs) -> ContextManager[None]: + """ + Build a pandas option context manager from keyword options. + + Args: + **kwargs: Pandas option names mapped to values. + + Returns: + ContextManager[None]: Context manager that applies the provided options. + """ options = flatten(kwargs.items()) return pd.option_context(*options) # type: ignore diff --git a/aymurai/utils/download.py b/aymurai/utils/download.py index 17151f32..52d2a7f3 100644 --- a/aymurai/utils/download.py +++ b/aymurai/utils/download.py @@ -1,6 +1,9 @@ import os +import shutil +import tempfile +from pathlib import Path -import gdown +import requests from aymurai.logger import get_logger @@ -9,30 +12,39 @@ logger = get_logger(__name__) -def download(url, output): +def download(url: str, output: str) -> str: """ - Download file from url - skip if file exists and environment variable NO_DOWNLOAD_IF_EXISTS is set to True + Stream download to a file. + + Skips download when the target exists and NO_DOWNLOAD_IF_EXISTS is truthy. Args: - url (str): url to download - output (str): output path + url (str): URL to download. + output (str): Path to save the downloaded file. Returns: - str: output path + str: Path to the downloaded file. + + Raises: + requests.HTTPError: If the download request returns an HTTP error status. """ - if os.path.exists(output) and bool(NO_DOWNLOAD_IF_EXISTS): - logger.warn( + output_path = Path(output) + output_path.parent.mkdir(parents=True, exist_ok=True) + + if output_path.exists() and NO_DOWNLOAD_IF_EXISTS: + logger.warning( "File found and skipping. Set NO_DOWNLOAD_IF_EXISTS environment to false to force download." ) - return output - - gdown.download( - url, - quiet=False, - fuzzy=True, - resume=True, - output=output, - ) - return output + return str(output_path) + + logger.info(f"Downloading {url} -> {output_path}") + with requests.get(url, stream=True, allow_redirects=True, timeout=60) as resp: + resp.raise_for_status() + with tempfile.NamedTemporaryFile(delete=False) as tmp: + for chunk in resp.iter_content(chunk_size=8192): + if chunk: + tmp.write(chunk) + tmp_path = Path(tmp.name) + shutil.move(tmp_path, output_path) + return str(output_path) diff --git a/aymurai/utils/entity_disambiguation/__init__.py b/aymurai/utils/entity_disambiguation/__init__.py new file mode 100644 index 00000000..1cfebd3d --- /dev/null +++ b/aymurai/utils/entity_disambiguation/__init__.py @@ -0,0 +1,16 @@ +from aymurai.utils.entity_disambiguation.core import ( + assign_label_instances, + map_canonical_entities_ner_preds, +) +from aymurai.utils.entity_disambiguation.fuzzy import ( + build_canonical_entities, +) + +from aymurai.utils.entity_disambiguation.date_formatter import get_canonical_dates + +__all__ = [ + "assign_label_instances", + "build_canonical_entities", + "map_canonical_entities_ner_preds", + "get_canonical_dates", +] diff --git a/aymurai/utils/entity_disambiguation/core.py b/aymurai/utils/entity_disambiguation/core.py new file mode 100644 index 00000000..ffa156fc --- /dev/null +++ b/aymurai/utils/entity_disambiguation/core.py @@ -0,0 +1,108 @@ +import copy +import uuid + +from aymurai.meta.api_interfaces import DocumentAnnotations +from aymurai.meta.entities import CanonicalEntities + + +def assign_label_instances( + predictions: DocumentAnnotations, +) -> DocumentAnnotations: + """ + Assigns ordered label instance indices (e.g., 1, 2) by appearance. + + Args: + predictions (DocumentAnnotations): Predictions with canonical IDs assigned. + + Returns: + DocumentAnnotations: Updated predictions with `aymurai_label_instance`. + """ + predictions_with_instances = copy.deepcopy(predictions) + + next_index_by_label: dict[str, int] = {} + instance_by_label_and_id: dict[tuple[str, str], int] = {} + + for document in predictions_with_instances: + if not document.labels: + continue + + for label in document.labels: + label_name = label.attrs.aymurai_label + entity_id = label.attrs.canonical_entity_id + + if not label_name or entity_id is None: + continue + + key = (label_name, str(entity_id)) + if key not in instance_by_label_and_id: + next_index_by_label[label_name] = ( + next_index_by_label.get(label_name, 0) + 1 + ) + instance_by_label_and_id[key] = next_index_by_label[label_name] + + label.attrs.aymurai_label_instance = instance_by_label_and_id[key] + + return predictions_with_instances + + +def map_canonical_entities_ner_preds( + predictions: DocumentAnnotations, + canonical_entities: CanonicalEntities, + *, + include_label_instances: bool = True, +) -> DocumentAnnotations: + """ + Applies canonical entity IDs and roles back onto NER predictions. + + Args: + predictions (DocumentAnnotations): Original predictions to update. + canonical_entities (CanonicalEntities): Canonical entities with IDs/roles. + include_label_instances (bool, optional): Whether to assign ordered label + instance indices (e.g., 1, 2). Defaults to True. + + Returns: + DocumentAnnotations: Updated predictions with canonical IDs, roles, and + optionally `aymurai_label_instance`. + """ + predictions_mapped = copy.deepcopy(predictions) + + new_ids_map = {} + + for document in predictions_mapped: + if not document.labels: + continue + + for label in document.labels: + if not label.attrs: + continue + + for ce in canonical_entities: + if label.attrs.aymurai_label == ce.aymurai_label: + entity_id = ce.entity_id + aliases = ce.aliases + + clean_aliases = [str(a).strip().lower() for a in aliases] + label_text = ( + str(label.attrs.aymurai_alt_text or label.text).strip().lower() + ) + + if label_text in clean_aliases: + label.attrs.canonical_entity_id = entity_id + break + + if label.attrs.canonical_entity_id is not None: + continue + + key = ( + label.attrs.aymurai_label, + str(label.attrs.aymurai_alt_text).strip(), + ) + if key not in new_ids_map: + new_ids_map[key] = uuid.uuid4() + + label.attrs.canonical_entity_id = new_ids_map[key] + + if include_label_instances: + return assign_label_instances(predictions_mapped) + + return predictions_mapped diff --git a/aymurai/utils/entity_disambiguation/date_formatter.py b/aymurai/utils/entity_disambiguation/date_formatter.py new file mode 100644 index 00000000..23196409 --- /dev/null +++ b/aymurai/utils/entity_disambiguation/date_formatter.py @@ -0,0 +1,55 @@ +from aymurai.meta.api_interfaces import DocLabel +from aymurai.meta.entities import CanonicalEntity +import uuid + + +def get_canonical_dates(labels: list[DocLabel]) -> list[CanonicalEntity]: + """ + Groups date labels by their normalized day and month (if available) to create canonical entities. + + Args: + labels (list[DocLabel]): A list of document labels to process. + + Returns: + list[CanonicalEntity]: A list of canonical entities representing unique dates, + each with its aliases and attributes. + """ + groups = {} + + for label in labels: + if label.attrs.aymurai_label != "FECHA": + continue + + raw_date = label.attrs.aymurai_alt_text or label.text + norm_date = ( + max(label.attrs.aymurai_label_subclass) + if label.attrs.aymurai_label_subclass + else None + ) + + if norm_date: + parts = norm_date.split("/") + day = parts[0] + month = parts[1] + year = parts[2] if len(parts) > 2 else "1900" + if year == "1900": + day_month_key = f"{day}/{month}" + else: + day_month_key = f"{day}/{month}/{year}" + else: + day_month_key = uuid.uuid4() + + if day_month_key not in groups: + groups[day_month_key] = CanonicalEntity( + aymurai_label="FECHA", + canonical_text=raw_date, + aliases=[], + attributes={}, + ) + + if raw_date not in groups[day_month_key].aliases: + groups[day_month_key].aliases.append(raw_date) + + label.attrs.canonical_entity_id = groups[day_month_key].entity_id + + return list(groups.values()) diff --git a/aymurai/utils/entity_disambiguation/fuzzy.py b/aymurai/utils/entity_disambiguation/fuzzy.py new file mode 100644 index 00000000..e55850d1 --- /dev/null +++ b/aymurai/utils/entity_disambiguation/fuzzy.py @@ -0,0 +1,227 @@ +from collections import Counter +from typing import Iterable + +from rapidfuzz import process +from rapidfuzz.fuzz import token_set_ratio + +from aymurai.meta.api_interfaces import DocLabel +from aymurai.meta.entities import CanonicalEntity + +from aymurai.transforms.anonymization_postprocess.exact_labels import EXACT_LABELS + + +def _find_parent(parent: list[int], idx: int) -> int: + """ + Finds the root representative of a union-find set with path compression. + + Args: + parent (list[int]): Parent pointers for the union-find structure. + idx (int): Index to resolve to its root. + + Returns: + int: The root index for the provided `idx`. + """ + if parent[idx] == idx: + return idx + parent[idx] = _find_parent(parent, parent[idx]) + return parent[idx] + + +def _union_parent(parent: list[int], left: int, right: int) -> None: + """ + Unions two indices in a union-find structure. + + Args: + parent (list[int]): Parent pointers for the union-find structure. + left (int): First index to union. + right (int): Second index to union. + """ + root_left, root_right = _find_parent(parent, left), _find_parent(parent, right) + if root_left != root_right: + parent[root_right] = root_left + + +def _cluster_aliases_with_cdist( + *, + items: list[dict[str, str]], + threshold: int, +) -> list[list[tuple[str, str, str]]]: + """ + Clusters alias items using RapidFuzz pairwise similarity with union-find. + + Args: + items (list[dict[str, str]]): Items containing "text" and "aymurai_label". + threshold (int): Minimum similarity threshold for clustering. + + Returns: + list[list[tuple[str, str, str]]]: Clusters of (original, normalized, label). + """ + if not items: + return [] + + entities = [item.get("text", "") for item in items] + labels = [item.get("aymurai_label", "UNKNOWN") for item in items] + + normed = [str(e).lower().strip() if e else "" for e in entities] + + sim = process.cdist(normed, normed, scorer=token_set_ratio, score_cutoff=threshold) + + parent = list(range(len(normed))) + + n = len(normed) + for i in range(n): + for j in range(i + 1, n): + if sim[i][j] >= threshold: + _union_parent(parent, i, j) + + clusters_map: dict[int, list[tuple[str, str, str]]] = {} + for idx in range(n): + root = _find_parent(parent, idx) + clusters_map.setdefault(root, []).append( + (entities[idx], normed[idx], labels[idx]) + ) + + return list(clusters_map.values()) + + +def _parse_cluster_item(item: tuple[str, str, str]) -> tuple[str, str, str]: + """ + Reorders a tuple from (original, normalized, label) to (label, original, normalized). + + Args: + item (tuple[str, str, str]): Tuple in the form (original, normalized, label). + + Returns: + tuple[str, str, str]: Tuple in the form (label, original, normalized). + """ + orig, norm, label = item + return label, orig, norm + + +def _pick_cluster_label(parsed_items: list[tuple[str, str, str]]) -> str: + """ + Selects the most frequent label from a cluster. + + Args: + parsed_items (list[tuple[str, str, str]]): Items in (label, original, normalized). + + Returns: + str: The most common label in the cluster. + """ + labels = [label for label, _, _ in parsed_items] + return Counter(labels).most_common(1)[0][0] + + +def _pick_longest_alias(parsed_items: list[tuple[str, str, str]]) -> str: + """ + Picks a canonical text representation from a cluster. + + Args: + parsed_items (list[tuple[str, str, str]]): Items in (label, original, normalized). + + Returns: + str: The longest alias text in the cluster. + """ + return max(parsed_items, key=lambda item: len(item[1]))[1] + + +def _clusters_to_canonical_entities( + clusters: list[list[tuple[str, str, str]]], +) -> list[CanonicalEntity]: + """ + Converts clustered alias groups into CanonicalEntity objects. + + Args: + clusters (list[list[tuple[str, str, str]]]): Clusters of + (original, normalized, label). + + Returns: + list[CanonicalEntity]: Canonical entities derived from clusters. + """ + canonical_entities = [] + for cluster in clusters: + parsed = [_parse_cluster_item(item) for item in cluster] + label = _pick_cluster_label(parsed) + canonical_text = _pick_longest_alias(parsed) + aliases = sorted({orig for _, orig, _ in parsed}) + canonical_entities.append( + CanonicalEntity( + aymurai_label=label, + canonical_text=canonical_text, + aliases=aliases, + attributes={}, + ) + ) + return canonical_entities + + +def build_canonical_entities( + labels: Iterable[DocLabel], + *, + target_labels: set[str] | None = None, + threshold: int, +) -> list[CanonicalEntity]: + """ + Builds canonical entities by clustering label aliases per entity type. + + Args: + labels (Iterable[DocLabel]): NER labels to cluster. + target_labels (set[str] | None, optional): Label filter; if provided, + only these labels are clustered. Defaults to None. + threshold (int): Minimum similarity threshold for clustering. + + Returns: + list[CanonicalEntity]: Canonical entities built from clustered aliases. + """ + grouped: dict[str, list[dict[str, str]]] = {} + for label in labels: + attrs = label.attrs + if not attrs or not attrs.aymurai_label: + continue + if target_labels and attrs.aymurai_label not in target_labels: + continue + alias = attrs.aymurai_alt_text or label.text + + subclass_val = getattr(attrs, "aymurai_label_subclass", None) + + if isinstance(subclass_val, list): + exact_alias = subclass_val[-1] if subclass_val else alias + else: + exact_alias = subclass_val or alias + + grouped.setdefault(attrs.aymurai_label, []).append( + { + "text": alias, + "aymurai_label": attrs.aymurai_label, + "exact_alias": exact_alias, + } + ) + + canonical_entities: list[CanonicalEntity] = [] + for label_type, items in grouped.items(): + if label_type in EXACT_LABELS: + exact_groups = {} + for item in items: + exact_groups.setdefault(item["exact_alias"], []).append(item) + clusters = [ + [ + ( + item["text"], + str(item["exact_alias"]).lower().strip(), + item["aymurai_label"], + ) + for item in group_items + ] + for group_items in exact_groups.values() + ] + else: + clusters = _cluster_aliases_with_cdist( + items=items, + threshold=threshold, + ) + + canonical_entities.extend(_clusters_to_canonical_entities(clusters)) + + canonical_entities = sorted(canonical_entities, key=lambda x: x.canonical_text) + + return canonical_entities diff --git a/aymurai/utils/json_data.py b/aymurai/utils/json_data.py index a28db543..b69bf5d6 100644 --- a/aymurai/utils/json_data.py +++ b/aymurai/utils/json_data.py @@ -1,20 +1,80 @@ import json +from datetime import date, datetime from itertools import groupby -from typing import Union, Iterable, Iterator +from typing import Any, Iterable, Iterator -def save_json(json_data: Union[dict, list[dict]], file_path: str): +def json_serial(obj: Any) -> str: + """ + JSON serializer for objects not serializable by default JSON encoder. + + Args: + obj (Any): The object to serialize. This function currently supports + datetime.date and datetime.datetime objects. + + Returns: + str: The ISO 8601 formatted string representation of the date or datetime object. + + Raises: + TypeError: If the object is not of type datetime.date or datetime.datetime. + """ + if isinstance(obj, (datetime, date)): + return obj.isoformat() + raise TypeError(f"Type {type(obj)} not serializable") + + +def get_pretty(obj: dict | list[Any]) -> str: + """ + Returns a pretty json string. + + Args: + obj (dict | list[Any]): The object to be converted to JSON. + + Returns: + str: The pretty JSON string. + """ + return json.dumps(obj, indent=4, ensure_ascii=False, default=json_serial) + + +def save_json(json_data: dict | list[dict], file_path: str) -> None: + """ + Saves a JSON object to a file. + + Args: + json_data (dict | list[dict]): The JSON data to be saved. + file_path (str): The path to the file where the JSON data will be saved. + """ with open(file_path, "w") as f: - f.write(json.dumps(json_data, indent=4, ensure_ascii=False)) + f.write( + json.dumps(json_data, indent=4, ensure_ascii=False, default=json_serial) + ) + + +def load_json(json_file_path: str) -> dict | list[dict]: + """ + Loads a JSON object from a file. + Args: + json_file_path (str): The path to the JSON file. -def load_json(json_file_path: str) -> Union[dict, list[dict]]: + Returns: + dict | list[dict]: The loaded JSON object. + """ with open(json_file_path, "r") as f: content = json.loads(f.read()) return content def get_unique(json_list: list[dict]) -> Iterator[dict]: + """ + Get unique json objects from a list of json objects. + + Args: + json_list (list[dict]): The list of JSON objects. + + Returns: + Iterator[dict]: An iterator of unique JSON objects. + """ unique = set(map(json.dumps, json_list)) unique = map(json.loads, unique) return unique @@ -23,7 +83,18 @@ def get_unique(json_list: list[dict]) -> Iterator[dict]: def group_by_key( json_iter: Iterable[dict], group_key: str, sort_key: str = "" ) -> Iterator[list[dict]]: + """ + Groups a list of JSON objects by a specified key. + + Args: + json_iter (Iterable[dict]): An iterable of JSON objects to be grouped. + group_key (str): The key to group the JSON objects by. + sort_key (str, optional): An optional key to sort the JSON objects before grouping. + Defaults to "". + Returns: + Iterator[list[dict]]: An iterator of lists of JSON objects grouped by the specified key. + """ if sort_key: json_iter = sorted(json_iter, key=lambda x: x[sort_key]) diff --git a/aymurai/utils/json_encoding.py b/aymurai/utils/json_encoding.py index c4b2e930..b5554812 100644 --- a/aymurai/utils/json_encoding.py +++ b/aymurai/utils/json_encoding.py @@ -3,7 +3,9 @@ import json import decimal import datetime +from typing import Any +import numpy as np import pandas as pd from aymurai.logger import get_logger @@ -12,7 +14,22 @@ class EnhancedJSONEncoder(json.JSONEncoder): - def default(self, obj): + """JSON encoder with support for datetime, timedelta, decimal, and NA values.""" + + def default(self, obj: Any) -> Any: + """ + Encode unsupported Python objects into JSON-serializable payloads. + + Args: + obj (Any): Object to encode. + + Returns: + Any: JSON-serializable representation of `obj`. + + Raises: + TypeError: If `obj` cannot be encoded by the custom logic or parent + JSON encoder. + """ if isinstance(obj, datetime.datetime): ARGS = ("year", "month", "day", "hour", "minute", "second", "microsecond") return { @@ -44,6 +61,10 @@ def default(self, obj): str(obj), ], } + elif isinstance(obj, np.integer): + return int(obj) + elif isinstance(obj, np.floating): + return float(obj) elif pd.isna(obj): return "null" else: @@ -55,10 +76,28 @@ def default(self, obj): class EnhancedJSONDecoder(json.JSONDecoder): - def __init__(self, *args, **kwargs): + """JSON decoder that reconstructs objects serialized by `EnhancedJSONEncoder`.""" + + def __init__(self, *args, **kwargs) -> None: + """ + Initialize decoder with a custom object hook. + + Args: + *args: Positional arguments forwarded to `json.JSONDecoder`. + **kwargs: Keyword arguments forwarded to `json.JSONDecoder`. + """ super().__init__(*args, object_hook=self.object_hook, **kwargs) - def object_hook(self, d): + def object_hook(self, d: dict) -> Any: + """ + Decode typed payloads into native Python objects. + + Args: + d (dict): Decoded dictionary from JSON. + + Returns: + Any: Reconstructed object when `__type__` is present, else `d`. + """ if "__type__" not in d: return d o = sys.modules[__name__] diff --git a/aymurai/utils/misc.py b/aymurai/utils/misc.py index 286d30f4..6d665e80 100644 --- a/aymurai/utils/misc.py +++ b/aymurai/utils/misc.py @@ -1,25 +1,31 @@ import re -from typing import Any, Union +from typing import Any def get_element( - obj, - levels: Union[list, Any] = [], + obj: Any, + levels: list[Any] | Any = [], default: Any = None, *, ignore_errors: bool = True, -): +) -> Any: """ - retrieve element hierarchically + Retrieve an element from a nested object using hierarchical keys. Args: - obj (object): parent object to retrieve to - levels (Union[list, Any], optional): hierarchy levels. Defaults to []. - default (Any, optional): default value to return - ignore_errors (str, optional): raise errors or ignore them. Defaults to True. + obj (Any): Parent object to traverse. + levels (list[Any] | Any, optional): Hierarchy levels to access. Defaults to []. + default (Any, optional): Value returned when traversal fails and + `ignore_errors` is True. Defaults to None. + ignore_errors (bool, optional): Whether to suppress lookup errors. + Defaults to True. Returns: - _type_: element or None (in case child element doesnt exist and `ignore_errors=True`) + Any: Retrieved element, or `default` when traversal fails and + `ignore_errors` is True. + + Raises: + Exception: Propagates the underlying error when `ignore_errors` is False. """ # if levels not a list handle it has a key @@ -40,7 +46,16 @@ def get_element( raise -def is_url(text: str): +def is_url(text: str) -> bool: + """ + Check whether a string contains a URL-like pattern. + + Args: + text (str): Text to evaluate. + + Returns: + bool: True when a URL-like pattern is found, otherwise False. + """ match = re.findall( r"(http(s)?:\/\/.)(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)", text, @@ -49,11 +64,16 @@ def is_url(text: str): # Taken from https://stackoverflow.com/a/20254842 -def get_recursively(search_dict: dict, field: str) -> list: +def get_recursively(search_dict: dict, field: str) -> list[Any]: """ - Takes a dict with nested lists and dicts, - and searches all dicts for a key of the field - provided. + Search nested dictionaries and lists for values under a target key. + + Args: + search_dict (dict): Dictionary to search recursively. + field (str): Key name to collect. + + Returns: + list[Any]: Values found for `field` across the nested structure. """ fields_found = [] diff --git a/aymurai/utils/pickle_data.py b/aymurai/utils/pickle_data.py index 4d5ceebb..09169ab1 100644 --- a/aymurai/utils/pickle_data.py +++ b/aymurai/utils/pickle_data.py @@ -3,12 +3,28 @@ from typing import Any -def save_pickle(object_: Any, output_path: str): +def save_pickle(object_: Any, output_path: str) -> None: + """ + Serialize and save a Python object to a pickle file. + + Args: + object_ (Any): Object to serialize. + output_path (str): Output file path. + """ with open(output_path, "wb") as f: pickle.dump(object_, f) def load_pickle(input_path: str) -> Any: + """ + Load and deserialize a Python object from a pickle file. + + Args: + input_path (str): Input file path. + + Returns: + Any: Deserialized Python object. + """ with (open(input_path, "rb")) as f: object_ = pickle.load(f) return object_ diff --git a/aymurai/utils/yaml_data.py b/aymurai/utils/yaml_data.py new file mode 100644 index 00000000..97d30fbb --- /dev/null +++ b/aymurai/utils/yaml_data.py @@ -0,0 +1,28 @@ +import yaml + + +def load_yaml(file_path: str) -> dict: + """ + Loads a yaml file and returns its content as a dictionary. + + Args: + file_path (str): The path to the yaml file. + + Returns: + dict: The yaml file content. + """ + with open(file_path, "r") as f: + content = yaml.safe_load(f) + return content + + +def save_yaml(dict_data: dict, file_path: str) -> None: + """ + Saves a dictionary as a yaml file. + + Args: + dict_data (dict): The dictionary to be saved. + file_path (str): The path to the file where the yaml will be saved. + """ + with open(file_path, "w") as f: + f.write(yaml.dump(dict_data)) diff --git a/docker-compose.yml b/docker-compose.yml index 3ce7c536..0f7775ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,29 +1,72 @@ +x-api-lite: &api-lite + image: ghcr.io/aymurai/api:latest + build: + context: . + dockerfile: ./docker/api/Dockerfile + target: aymurai-api + shm_size: "2gb" + ports: + - "8899:8899" + volumes: + - ./resources/cache:/resources/cache + env_file: + - .env + - .env.common + +x-api-full: &api-full + image: ghcr.io/aymurai/api:full + build: + context: . + dockerfile: ./docker/api/Dockerfile + target: aymurai-api-full + shm_size: "2gb" + ports: + - "8899:8899" + restart: always + deploy: + resources: + limits: + memory: 4g + cpus: "4.0" + services: aymurai-api: - image: ghcr.io/aymurai/api:latest - ports: - - "8899:8899" - build: - context: . - dockerfile: ./docker/api/Dockerfile - target: aymurai-api - env_file: - - .env - - .env.common - volumes: - - ./resources/cache:/resources/cache + <<: *api-lite + container_name: aymurai-api + environment: + TORCH_DEVICE: cpu + + aymurai-api-gpu: + <<: *api-lite + container_name: aymurai-api-gpu + environment: + TORCH_DEVICE: cuda + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] aymurai-api-full: - image: ghcr.io/aymurai/api:full - ports: - - "8899:8899" - build: - context: . - dockerfile: ./docker/api/Dockerfile - target: aymurai-api-full - restart: always + <<: *api-full + container_name: aymurai-api-full + environment: + TORCH_DEVICE: cpu + + aymurai-api-full-gpu: + <<: *api-full + container_name: aymurai-api-full-gpu + environment: + TORCH_DEVICE: cuda deploy: resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] limits: memory: 4g cpus: "4.0" diff --git a/docker/api/Dockerfile b/docker/api/Dockerfile index 660e5a7e..1ac9d054 100644 --- a/docker/api/Dockerfile +++ b/docker/api/Dockerfile @@ -1,3 +1,24 @@ +# syntax=docker/dockerfile:1.7 + +# ------------------- +# Frontend Build Stage +# ------------------- +FROM node:lts-bookworm AS frontend-builder + +WORKDIR /frontend + +RUN npm install --global corepack@latest && \ + corepack enable pnpm && \ + corepack prepare pnpm@latest --activate + +COPY frontend/ ./ + +RUN --mount=type=cache,target=/root/.local/share/pnpm/store \ + pnpm install --frozen-lockfile + +RUN pnpm run build:web + + # ------------------- # Build Stage # ------------------- @@ -19,7 +40,7 @@ WORKDIR /app RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ - uv sync --frozen --no-install-project --extra runtime + uv sync --frozen --no-install-project # Copy application code COPY aymurai /app/aymurai @@ -73,12 +94,14 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ COPY --from=builder --chown=app:app /app/.venv /app/.venv # Copy static resources -COPY resources/api/static /resources/api/static COPY resources/pipelines /resources/pipelines +COPY --from=frontend-builder /frontend/out/renderer /app/frontend-dist # Set working directory WORKDIR /app +ENV FRONTEND_DIST_DIR=/app/frontend-dist + # Define start command CMD uv run fastapi run --port 8899 .venv/lib/python3.10/site-packages/aymurai/api/main.py @@ -88,13 +111,10 @@ CMD uv run fastapi run --port 8899 .venv/lib/python3.10/site-packages/aymurai/ap # ----------------------------- FROM aymurai-api AS aymurai-api-full -# Configure TensorFlow and resources -ENV TF_CPP_MIN_LOG_LEVEL=3 \ - TFHUB_CACHE_DIR=resources/cache/tfhub_modules \ - RESOURCES_BASEPATH=resources +# Configure resources base path +ENV RESOURCES_BASEPATH=resources # Copy additional resources -COPY ./resources/api resources/api COPY ./resources/pipelines/production resources/pipelines/production # Initialize application to download models diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 6d7efff6..0d55d3f7 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,66 +1,101 @@ # Contributing to AymurAI -We are happy to accept your contributions to make `AymurAI` better and more awesome! To avoid unnecessary work on either -side, please stick to the following process: +We are happy to accept contributions that help make `AymurAI` more useful, more robust, and easier to maintain. +To avoid unnecessary work on either side, please use the following flow. -1. Check if there is already [an issue](https://github.com/AymurAI/dev/issues) for your concern. -2. If there is not, open a new one to start a discussion. We hate to close finished PRs! -3. If we decide your concern needs code changes, we would be happy to accept a pull request. Please consider the -commit guidelines below. +## Contribution flow +1. Check whether there is already an issue for your topic: +2. If not, open a new issue with context, motivation, and expected outcome. +3. Once the scope is clear, submit a pull request tied to that issue. -In case you just want to help out and don't know where to start, -[issues with "help wanted" label](https://github.com/AymurAI/dev/labels/help%20wanted) are good for -first-time contributors. +If you want to help and do not know where to start, small documentation fixes, test coverage improvements, and cleanup PRs are all welcome. +## Local development +If you want to get deeper into the API, we recommend cloning the repository and running the stack locally. +The codebase is fairly navigable, and most of the important modules are documented or organized by workflow. +### Option A: Docker (recommended) +You can use the provided compose services directly: -## Developing locally +```bash +make api-up +# or make api-full-up +``` -For contributors looking to get deeper into the API we suggest cloning the repository. -Nearly all classes and methods are documented, so finding your way around -the code should hopefully be easy. +Bundled frontend: `http://localhost:8899/` -### Setup +Swagger UI: `http://localhost:8899/api/docs` -#### Using Docker and devcontainer (recommended) -You can use the provided `devcontainer` load all the tools and packages needed. This can be done directly from Visual Studio Code. -You can check the [devcontainer documentation](https://code.visualstudio.com/docs/remote/containers) for more information. +If you prefer working from VS Code, the repository also includes a `.devcontainer/` setup. + +### Option B: Local Python environment +Repository requires Python `3.10`. -#### Using jupyterlab image -If you want just check the notebooks and tutorials you can use the `jupyterlab` docker image. To run the image in `gpu` mode run: -```bash -make jupyter-run -``` -alternatively you can run in `cpu` mode with: ```bash -make jupyter-run-cpu +# if using uv +uv sync --all-groups + +# fallback with pip +pip install -e . ``` +For most contributors, Docker is the easiest way to get a working API with the expected runtime dependencies. + +### Frontend development +The frontend supports browser and Electron development. Install its dependencies and run the desired target: -#### Install direclty on a your python environment -create a python environment of your preference and run: ```bash -pip install src/aymurai +cd frontend +pnpm install +pnpm run dev:web # browser +pnpm run dev # Electron ``` -You may need to install redis or run it in a docker container. You can use the following command to run it in a docker container: +Before submitting frontend changes, run: + ```bash -make redis-run +pnpm run lint +pnpm run typecheck +pnpm test ``` -### Git pre-commit Hooks -After installing the dependencies, install `pre-commit` hooks via: +See [the frontend README](../frontend/README.md) for build and packaging commands. + +## Pre-commit hooks +After installing dependencies, enable the hooks: + ```bash pre-commit install ``` -This will automatically run code formatters black and isort for each git commit. Also it will clear all outputs from the notebooks. If you want to more information about why we do this, please refer to the [data security](docs/DATA_SECURITY.md) section. +Configured hooks currently include: +- `ruff` +- `ruff-format` +- `nbstripout` +This helps keep code formatting consistent and prevents notebook output from leaking into commits. -### Code Formatting +## Formatting +If needed, you can run the formatter manually before committing: -To ensure a standardized code style we use the formatter [black](https://github.com/ambv/black) and for standardizing imports we use [isort](https://github.com/PyCQA/isort). -If your code is not formatted properly, the tests will fail. +```bash +ruff format aymurai/ +``` -If you set up pre-commit hooks, every git commit will automatically run these formatters. Otherwise you can also manually run them, or let your IDE run them on every file save. -Running from the command line works via `black src/aymurai/ && isort src/aymurai/` in the repository root folder. +## Documentation policy +When behavior changes in the frontend, API, pipelines, or DB persistence, update the corresponding docs in the same PR: +- `README.md` +- `frontend/README.md` +- `docs/api/README.md` +- `docs/pipelines/README.md` +- `docs/pipelines/anonymizer/README.md` +- `docs/pipelines/datapublic/README.md` +- `docs/database/README.md` +- `docs/entities/README.md` +- `docs/models/README.md` + +If the change is user-facing, documentation should land together with the code. + +## Community and security +- Code of conduct: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) +- Security and ethics: [SECURITY.md](SECURITY.md) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..fa347452 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,29 @@ +# Documentation Index +Language: **English** | [Español](es/README.md) + +This index covers operational documentation for `v1.5.0`. + +## Core docs +- Repository entrypoint: [../README.md](../README.md) +- API reference: [api/README.md](api/README.md) +- Pipelines index: [pipelines/README.md](pipelines/README.md) +- Anonymizer flow: [pipelines/anonymizer/README.md](pipelines/anonymizer/README.md) +- Datapublic flow: [pipelines/datapublic/README.md](pipelines/datapublic/README.md) +- Internal database schema: [database/README.md](database/README.md) +- Frontend development and packaging: [../frontend/README.md](../frontend/README.md) + +## Entities +- Entities index: [entities/README.md](entities/README.md) +- Datapublic entities: [entities/datapublic/README.md](entities/datapublic/README.md) +- Anonymizer entities: [entities/anonymizer/README.md](entities/anonymizer/README.md) + +## Models +- Models index: [models/README.md](models/README.md) +- Anonymizer NER model card: [models/anonymizer-model-card.md](models/anonymizer-model-card.md) +- Flair NER model card: [models/flair-model-card.md](models/flair-model-card.md) +- Decision model card: [models/decision-model-card.md](models/decision-model-card.md) + +## Governance +- Contributing: [CONTRIBUTING.md](CONTRIBUTING.md) +- Security and ethics: [SECURITY.md](SECURITY.md) +- Code of conduct: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 7e95a465..72aa9d8a 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -11,4 +11,4 @@ AymurAI is guided by a strong ethical position that considers the complexities o We acknowledge that every line of code that we write may potentially contain security issues. We are trying to deal with it responsibly and provide patches as quickly as possible. ## Reporting a Vulnerability -If you have found a security vulnerability, please open a new [issue](https://github.com/AymurAI/dev/issues/new) or report it to us by sending an [email](aymurai@datagenero.org). We will respond to you as soon as possible. \ No newline at end of file +If you have found a security vulnerability, please open a new [issue](https://github.com/AymurAI/backend/issues/new) or report it to us by sending an [email](mailto:aymurai@datagenero.org). We will respond to you as soon as possible. diff --git a/docs/anonymization/pipeline.excalidraw b/docs/anonymization/pipeline.excalidraw new file mode 100644 index 00000000..358c538e --- /dev/null +++ b/docs/anonymization/pipeline.excalidraw @@ -0,0 +1,3892 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", + "elements": [ + { + "id": "mprOUEQImobIiF7TwmSu5", + "type": "rectangle", + "x": 323.33333333333337, + "y": 212.14179104477614, + "width": 215.66666666666663, + "height": 280.0447761194029, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6mIBkNFpjb5VS1uhyvbNS" + ], + "frameId": null, + "index": "a0", + "roundness": { + "type": 3 + }, + "seed": 1252894395, + "version": 114, + "versionNonce": 929231579, + "isDeleted": false, + "boundElements": [ + { + "id": "WOvaiSIhXDUuFwvEHM9bo", + "type": "arrow" + } + ], + "updated": 1764873895187, + "link": null, + "locked": false + }, + { + "id": "j4e8O-DqInLIe6FPmm6uf", + "type": "line", + "x": 366.7885572139304, + "y": 252.37810945273628, + "width": 94.95771144278605, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6mIBkNFpjb5VS1uhyvbNS" + ], + "frameId": null, + "index": "a1", + "roundness": { + "type": 2 + }, + "seed": 1993017845, + "version": 82, + "versionNonce": 612513819, + "isDeleted": false, + "boundElements": null, + "updated": 1764873895188, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 94.95771144278605, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "VvUrRcwOayuSCWD_BEIZ-", + "type": "line", + "x": 362.4084692677094, + "y": 279.52565900841427, + "width": 48.28358208955214, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6mIBkNFpjb5VS1uhyvbNS" + ], + "frameId": null, + "index": "a2", + "roundness": { + "type": 2 + }, + "seed": 1917498107, + "version": 185, + "versionNonce": 1834646715, + "isDeleted": false, + "boundElements": [], + "updated": 1764873895188, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 48.28358208955214, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "XF0GuSC8OHCflsYFqZIj-", + "type": "line", + "x": 362.95503103842134, + "y": 309.29823770215256, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6mIBkNFpjb5VS1uhyvbNS" + ], + "frameId": null, + "index": "a3", + "roundness": { + "type": 2 + }, + "seed": 954781173, + "version": 215, + "versionNonce": 1208036699, + "isDeleted": false, + "boundElements": [], + "updated": 1764873895188, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "ePQ9UG1U9o78Pyz1DIpJG", + "type": "text", + "x": 376.4452736318408, + "y": 538.8606965174129, + "width": 82.85454631444826, + "height": 80.47263681592038, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6mIBkNFpjb5VS1uhyvbNS" + ], + "frameId": null, + "index": "a4", + "roundness": null, + "seed": 899224181, + "version": 96, + "versionNonce": 1204341243, + "isDeleted": false, + "boundElements": null, + "updated": 1764873895188, + "link": null, + "locked": false, + "text": ".docx\n.pdf", + "fontSize": 32.189054726368155, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": ".docx\n.pdf", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "50Qya96m5ryhBOeNuo5sd", + "type": "line", + "x": 361.96019900497515, + "y": 337.67910447761193, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6mIBkNFpjb5VS1uhyvbNS" + ], + "frameId": null, + "index": "a5", + "roundness": { + "type": 2 + }, + "seed": 1599168565, + "version": 113, + "versionNonce": 1907610267, + "isDeleted": false, + "boundElements": null, + "updated": 1764873895188, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "euPm7o0scLf0nKXvH2Mi-", + "type": "line", + "x": 369.12473982912934, + "y": 364.5779783514023, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6mIBkNFpjb5VS1uhyvbNS" + ], + "frameId": null, + "index": "a6", + "roundness": { + "type": 2 + }, + "seed": 1599660891, + "version": 147, + "versionNonce": 108316475, + "isDeleted": false, + "boundElements": [], + "updated": 1764873895188, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "RTz0Ti8oU16rW7TBEfaWk", + "type": "line", + "x": 369.4772828409415, + "y": 390.08391000778613, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6mIBkNFpjb5VS1uhyvbNS" + ], + "frameId": null, + "index": "a7", + "roundness": { + "type": 2 + }, + "seed": 1240160981, + "version": 253, + "versionNonce": 208545755, + "isDeleted": false, + "boundElements": [], + "updated": 1764873895188, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "ovlQj2YD-86eShmKhZsbR", + "type": "line", + "x": 368.4824508074953, + "y": 418.4647767832455, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6mIBkNFpjb5VS1uhyvbNS" + ], + "frameId": null, + "index": "a8", + "roundness": { + "type": 2 + }, + "seed": 1975728181, + "version": 151, + "versionNonce": 431125627, + "isDeleted": false, + "boundElements": [], + "updated": 1764873895188, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "EtWsq0p_WPVrVIpbECqaR", + "type": "line", + "x": 375.6469916316496, + "y": 445.36365065703586, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6mIBkNFpjb5VS1uhyvbNS" + ], + "frameId": null, + "index": "a9", + "roundness": { + "type": 2 + }, + "seed": 158886293, + "version": 185, + "versionNonce": 1577331995, + "isDeleted": false, + "boundElements": [], + "updated": 1764873895188, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "WOvaiSIhXDUuFwvEHM9bo", + "type": "arrow", + "x": 544.801292623903, + "y": 335.6285710087246, + "width": 276.37331055069967, + "height": 0.9929830550302654, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aA", + "roundness": { + "type": 2 + }, + "seed": 910188603, + "version": 458, + "versionNonce": 376827547, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "2tskJJoi5g4IGXcOp3rfQ" + } + ], + "updated": 1764875808259, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 276.37331055069967, + -0.9929830550302654 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "mprOUEQImobIiF7TwmSu5", + "focus": -0.11485898447026521, + "gap": 5.801292623903009 + }, + "endBinding": { + "elementId": "R8DpmX_gFC_B_zQb3gs_d", + "focus": 0.058585250919652596, + "gap": 26.273809523809632 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "2tskJJoi5g4IGXcOp3rfQ", + "type": "text", + "x": 600.4880623401708, + "y": 310.1320794812094, + "width": 164.99977111816406, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aB", + "roundness": null, + "seed": 773447163, + "version": 42, + "versionNonce": 491638229, + "isDeleted": false, + "boundElements": null, + "updated": 1764875807159, + "link": null, + "locked": false, + "text": "/misc/document-\nextract", + "fontSize": 20, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "WOvaiSIhXDUuFwvEHM9bo", + "originalText": "/misc/document-extract", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "R8DpmX_gFC_B_zQb3gs_d", + "type": "text", + "x": 847.4484126984123, + "y": 224.08730158730157, + "width": 536.5656565656566, + "height": 232.7272727272728, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aE", + "roundness": null, + "seed": 890582459, + "version": 272, + "versionNonce": 1768355637, + "isDeleted": false, + "boundElements": [ + { + "id": "WOvaiSIhXDUuFwvEHM9bo", + "type": "arrow" + } + ], + "updated": 1764874730850, + "link": null, + "locked": false, + "text": "{\n \"document_id\": ,\n \"document\": [\n \"R., MATIAS EZEQUIEL SOBRE\n128 1 párr\",\n ...\n ]\n}", + "fontSize": 23.27272727272728, + "fontFamily": 8, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "{\n \"document_id\": ,\n \"document\": [\n \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n ...\n ]\n}", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "id": "9wZT6gadfEtFJsnK6a38y", + "type": "arrow", + "x": 1160.80487286459, + "y": 452.14285714285705, + "width": 348.62970232164366, + "height": 56.216370515816834, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aF", + "roundness": { + "type": 2 + }, + "seed": 1750164379, + "version": 998, + "versionNonce": 411091643, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "1kzq54A47HhfpjGS19GQs" + } + ], + "updated": 1764876692331, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 151.88163507191734, + 49.64285714285717 + ], + [ + 348.62970232164366, + 56.216370515816834 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": { + "elementId": "XNFeqRFFVQOQ2Amv9KiKf", + "focus": -0.20762051337520257, + "gap": 22.16146671210572 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false, + "fixedSegments": [ + { + "index": 2, + "start": [ + 0, + 123.33333333333314 + ], + "end": [ + 111.76666666666688, + 123.33333333333314 + ] + }, + { + "index": 3, + "start": [ + 111.76666666666688, + 123.33333333333314 + ], + "end": [ + 111.76666666666688, + 110.84286406975184 + ] + } + ], + "startIsSpecial": false, + "endIsSpecial": false + }, + { + "id": "1kzq54A47HhfpjGS19GQs", + "type": "text", + "x": 1208.1866528950034, + "y": 489.2857142857142, + "width": 208.9997100830078, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aFV", + "roundness": null, + "seed": 1033291995, + "version": 30, + "versionNonce": 40809307, + "isDeleted": false, + "boundElements": null, + "updated": 1764876692331, + "link": null, + "locked": false, + "text": "/anonymizer/predict", + "fontSize": 20, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "9wZT6gadfEtFJsnK6a38y", + "originalText": "/anonymizer/predict", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "XNFeqRFFVQOQ2Amv9KiKf", + "type": "rectangle", + "x": 1526.4365079365075, + "y": 256.86507936507934, + "width": 521.6666666666664, + "height": 428.88888888888886, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aH", + "roundness": { + "type": 3 + }, + "seed": 477388725, + "version": 215, + "versionNonce": 1294039099, + "isDeleted": false, + "boundElements": [ + { + "id": "dHelCwKVUG5VTmm5vyDz_", + "type": "arrow" + }, + { + "id": "X1lDBhgBqAfbBo2Wh3tA1", + "type": "arrow" + }, + { + "id": "9wZT6gadfEtFJsnK6a38y", + "type": "arrow" + } + ], + "updated": 1764874713403, + "link": null, + "locked": false + }, + { + "id": "DDpm2VgzVTuFYgzzSeDoT", + "type": "text", + "x": 1698.3106526268848, + "y": 697.4206349206348, + "width": 127.13986206054688, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aI", + "roundness": null, + "seed": 781671541, + "version": 183, + "versionNonce": 198505051, + "isDeleted": false, + "boundElements": [ + { + "id": "qJLbJGOfzrt9cwONItkng", + "type": "arrow" + } + ], + "updated": 1764875154944, + "link": null, + "locked": false, + "text": "NER outputs", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "NER outputs", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "qJLbJGOfzrt9cwONItkng", + "type": "arrow", + "x": 1759.2849840986328, + "y": 751.4206349206347, + "width": 5.191199117050928, + "height": 178.5, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aJ", + "roundness": { + "type": 2 + }, + "seed": 1942697813, + "version": 481, + "versionNonce": 1156232859, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "i_EvQstHW-Ge2wvbLPeQ_" + } + ], + "updated": 1764875159518, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 5.191199117050928, + 178.5 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "DDpm2VgzVTuFYgzzSeDoT", + "focus": 0.05947616440110493, + "gap": 14 + }, + "endBinding": { + "elementId": "QMg1bW0iJLThqV1wPLnRu", + "focus": -0.39612694313383184, + "gap": 1 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "i_EvQstHW-Ge2wvbLPeQ_", + "type": "text", + "x": 1654.8938758278896, + "y": 786.9206349206347, + "width": 204.59686279296875, + "height": 70, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aJG", + "roundness": null, + "seed": 1379805179, + "version": 123, + "versionNonce": 1293571995, + "isDeleted": false, + "boundElements": null, + "updated": 1764875154945, + "link": null, + "locked": false, + "text": "fuzzy matching\napproach", + "fontSize": 28, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "qJLbJGOfzrt9cwONItkng", + "originalText": "fuzzy matching approach", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "QMg1bW0iJLThqV1wPLnRu", + "type": "text", + "x": 1498.6190476190475, + "y": 929.9206349206347, + "width": 905.5525030525026, + "height": 372.5, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aL", + "roundness": null, + "seed": 101534011, + "version": 624, + "versionNonce": 869890651, + "isDeleted": false, + "boundElements": [ + { + "id": "qJLbJGOfzrt9cwONItkng", + "type": "arrow" + }, + { + "id": "ZMKT-Hy2-Yjk8g0ssbD0q", + "type": "arrow" + } + ], + "updated": 1764876718291, + "link": null, + "locked": false, + "text": "[\n {\n \"aymurai_label\": \"PER\",\n \"canonical_text\": \"MATIAS EZEQUIEL R.\",\n \"aliases\": [\n \"MATIAS EZEQUIEL R.\",\n \"R., MATÍAS EZEQUIEL\",\n \"Matías Ezequiel R.\"\n ],\n \"attributes\": {}\n \n }\n]", + "fontSize": 22.923076923076923, + "fontFamily": 8, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "[\n {\n \"aymurai_label\": \"PER\",\n \"canonical_text\": \"MATIAS EZEQUIEL R.\",\n \"aliases\": [\n \"MATIAS EZEQUIEL R.\",\n \"R., MATÍAS EZEQUIEL\",\n \"Matías Ezequiel R.\"\n ],\n \"attributes\": {}\n \n }\n]", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "id": "ZMKT-Hy2-Yjk8g0ssbD0q", + "type": "arrow", + "x": 1779.442822764371, + "y": 1253.9206349206347, + "width": 1.335206841328045, + "height": 212.22319297821105, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aM", + "roundness": { + "type": 2 + }, + "seed": 1658264629, + "version": 582, + "versionNonce": 1501063611, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "cPRb_-k-P_xgsphynZa-B" + } + ], + "updated": 1764876718291, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -1.335206841328045, + 212.22319297821105 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "QMg1bW0iJLThqV1wPLnRu", + "focus": 0.37688413952517746, + "gap": 14 + }, + "endBinding": { + "elementId": "HT4dCz_WP0No85IzXVlCJ", + "focus": 0.01747376853034287, + "gap": 14 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "cPRb_-k-P_xgsphynZa-B", + "type": "text", + "x": 1680.8807133568653, + "y": 1277.5159805629846, + "width": 186.99974060058594, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aMV", + "roundness": null, + "seed": 1547919451, + "version": 55, + "versionNonce": 979131387, + "isDeleted": false, + "boundElements": null, + "updated": 1764875143575, + "link": null, + "locked": false, + "text": "CanonicalEntities\n", + "fontSize": 20, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "ZMKT-Hy2-Yjk8g0ssbD0q", + "originalText": "CanonicalEntities\n", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "HT4dCz_WP0No85IzXVlCJ", + "type": "diamond", + "x": 1572.2377265143011, + "y": 1491.9307081807083, + "width": 404.2857142857141, + "height": 299.9999999999999, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aN", + "roundness": { + "type": 2 + }, + "seed": 474391803, + "version": 219, + "versionNonce": 917939349, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "6BbvmS0QuJA92jEgUAM7-" + }, + { + "id": "ZMKT-Hy2-Yjk8g0ssbD0q", + "type": "arrow" + }, + { + "id": "dHelCwKVUG5VTmm5vyDz_", + "type": "arrow" + } + ], + "updated": 1764876727910, + "link": null, + "locked": false + }, + { + "id": "6BbvmS0QuJA92jEgUAM7-", + "type": "text", + "x": 1748.381054499792, + "y": 1624.4307081807083, + "width": 51.856201171875, + "height": 35, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aO", + "roundness": null, + "seed": 827091643, + "version": 146, + "versionNonce": 459352021, + "isDeleted": false, + "boundElements": null, + "updated": 1764875170814, + "link": null, + "locked": false, + "text": "LLM", + "fontSize": 28, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "HT4dCz_WP0No85IzXVlCJ", + "originalText": "LLM", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "dHelCwKVUG5VTmm5vyDz_", + "type": "arrow", + "x": 2063.285201930389, + "y": 694.3135258107342, + "width": 474.8611910477862, + "height": 844.0773032721003, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aR", + "roundness": { + "type": 2 + }, + "seed": 2016671099, + "version": 642, + "versionNonce": 611520693, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "GTYZaUFv22_vAQroP4Ots" + } + ], + "updated": 1764876728139, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 290.8894012442138, + 434.06705471007433 + ], + [ + -183.9717898035724, + 844.0773032721003 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "XNFeqRFFVQOQ2Amv9KiKf", + "focus": -0.3128704896896401, + "gap": 28.317023962699135 + }, + "endBinding": { + "elementId": "HT4dCz_WP0No85IzXVlCJ", + "focus": -0.07412509117026206, + "gap": 24.87677840035615 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "GTYZaUFv22_vAQroP4Ots", + "type": "text", + "x": 2323.4000670417904, + "y": 1024.213913854142, + "width": 261.549072265625, + "height": 35, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aRV", + "roundness": null, + "seed": 1799358171, + "version": 29, + "versionNonce": 916759547, + "isDeleted": false, + "boundElements": null, + "updated": 1764873079510, + "link": null, + "locked": false, + "text": "Entities in context", + "fontSize": 28, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "dHelCwKVUG5VTmm5vyDz_", + "originalText": "Entities in context", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "fQd08AUXM4h1r1rt1GEYO", + "type": "arrow", + "x": 1531.7321526774883, + "y": 1643.1003241404883, + "width": 262.51194713959944, + "height": 2.3392319195597793, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aT", + "roundness": { + "type": 2 + }, + "seed": 860487771, + "version": 330, + "versionNonce": 363111093, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "NX_RkmyZ4ShoFSsJ4jTLw" + } + ], + "updated": 1764875170815, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -262.51194713959944, + -2.3392319195597793 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "NX_RkmyZ4ShoFSsJ4jTLw", + "type": "text", + "x": 576.9763088073955, + "y": 1091.9307081807083, + "width": 186.99974060058594, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aU", + "roundness": null, + "seed": 410991867, + "version": 62, + "versionNonce": 810030421, + "isDeleted": false, + "boundElements": [], + "updated": 1764875170815, + "link": null, + "locked": false, + "text": "CanonicalEntities\n", + "fontSize": 20, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "fQd08AUXM4h1r1rt1GEYO", + "originalText": "CanonicalEntities\n", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "IEM1W5vPfKwT3lgRgCi-R", + "type": "text", + "x": 694.7031440781441, + "y": 1395.8730158730161, + "width": 905.5525030525026, + "height": 487.1153846153846, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aW", + "roundness": null, + "seed": 202961365, + "version": 797, + "versionNonce": 518433403, + "isDeleted": false, + "boundElements": [ + { + "id": "ZMKT-Hy2-Yjk8g0ssbD0q", + "type": "arrow" + } + ], + "updated": 1764875186373, + "link": null, + "locked": false, + "text": "[\n {\n \"entity_id\": ,\n \"aymurai_label\": \"PER\",\n \"canonical_text\": \"MATIAS EZEQUIEL R.\",\n \"aliases\": [\n \"MATIAS EZEQUIEL R.\",\n \"R., MATÍAS EZEQUIEL\",\n \"Matías Ezequiel R.\"\n ],\n \"attributes\": {\n \"role\": \"Acusado/a \n }\n \n },\n ...\n]", + "fontSize": 22.923076923076923, + "fontFamily": 8, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "[\n {\n \"entity_id\": ,\n \"aymurai_label\": \"PER\",\n \"canonical_text\": \"MATIAS EZEQUIEL R.\",\n \"aliases\": [\n \"MATIAS EZEQUIEL R.\",\n \"R., MATÍAS EZEQUIEL\",\n \"Matías Ezequiel R.\"\n ],\n \"attributes\": {\n \"role\": \"Acusado/a \n }\n \n },\n ...\n]", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "id": "X1lDBhgBqAfbBo2Wh3tA1", + "type": "arrow", + "x": 1522.927714874569, + "y": 690.6888562602353, + "width": 1016.253111699966, + "height": 1419.4587535518317, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aY", + "roundness": { + "type": 2 + }, + "seed": 1525774261, + "version": 379, + "versionNonce": 916502267, + "isDeleted": false, + "boundElements": null, + "updated": 1764874776673, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -1016.253111699966, + 794.7674929461141 + ], + [ + -678.7729798846242, + 1419.4587535518317 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "XNFeqRFFVQOQ2Amv9KiKf", + "focus": -0.03022916805647842, + "gap": 17.297044401543584 + }, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "bLpxUpm3l_5AdE-wefxGf", + "type": "arrow", + "x": 1033.2850735335269, + "y": 1775.4563492063494, + "width": 0.08162462097448042, + "height": 270.3395345008357, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aZ", + "roundness": { + "type": 2 + }, + "seed": 1621637429, + "version": 264, + "versionNonce": 980985435, + "isDeleted": false, + "boundElements": null, + "updated": 1764876700324, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.08162462097448042, + 270.3395345008357 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": { + "elementId": "-PgdE-XG4rOV6rnyozIFX", + "focus": -0.010707089043139571, + "gap": 30.459273173000824 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "-PgdE-XG4rOV6rnyozIFX", + "type": "diamond", + "x": 762.8365384615385, + "y": 2065.4563492063494, + "width": 544.2857142857139, + "height": 342.49999999999994, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aa", + "roundness": { + "type": 2 + }, + "seed": 987028059, + "version": 397, + "versionNonce": 1724613083, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "Xwf273IrzwAMLQrLCkCOu" + }, + { + "id": "3VriZd7FuEOwMpdPbnIiX", + "type": "arrow" + }, + { + "id": "bLpxUpm3l_5AdE-wefxGf", + "type": "arrow" + } + ], + "updated": 1764876697912, + "link": null, + "locked": false + }, + { + "id": "Xwf273IrzwAMLQrLCkCOu", + "type": "text", + "x": 919.4074406047444, + "y": 2184.0813492063494, + "width": 231.0010528564453, + "height": 105, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ab", + "roundness": null, + "seed": 1570690811, + "version": 369, + "versionNonce": 1402964917, + "isDeleted": false, + "boundElements": [], + "updated": 1764875219055, + "link": null, + "locked": false, + "text": "Entity to\nCanonicalEntity\nmapping", + "fontSize": 28, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "-PgdE-XG4rOV6rnyozIFX", + "originalText": "Entity to CanonicalEntity mapping", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "YWHB-Nw2VxgFSkL-pcYdK", + "type": "text", + "x": 1035.5431163075204, + "y": 3213.0399522268453, + "width": 223.87255859375, + "height": 80.21844660194176, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "al", + "roundness": null, + "seed": 1428684437, + "version": 332, + "versionNonce": 1630622267, + "isDeleted": false, + "boundElements": [ + { + "id": "j-ZCuk6tMAZF7uiWTtIk4", + "type": "arrow" + } + ], + "updated": 1764875186373, + "link": null, + "locked": false, + "text": "Disambiguated\nNER outputs", + "fontSize": 32.087378640776706, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Disambiguated\nNER outputs", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "3VriZd7FuEOwMpdPbnIiX", + "type": "arrow", + "x": 1049.3582234833063, + "y": 2398.7390090918116, + "width": 56.8350216132967, + "height": 206.2823298539638, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "am", + "roundness": { + "type": 2 + }, + "seed": 479819349, + "version": 456, + "versionNonce": 528370965, + "isDeleted": false, + "boundElements": [], + "updated": 1764875219055, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 56.8350216132967, + 206.2823298539638 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "-PgdE-XG4rOV6rnyozIFX", + "focus": 0.11433848854056756, + "gap": 3.6805732632926023 + }, + "endBinding": { + "elementId": "RCTW7-Twmc0vzPbjJZlva", + "focus": 0.012854192325188162, + "gap": 17.9562212017031 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "j-ZCuk6tMAZF7uiWTtIk4", + "type": "arrow", + "x": 1149.7230307134546, + "y": 3304.34173216212, + "width": 0.0530047510710574, + "height": 179.03128371089633, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "an", + "roundness": { + "type": 2 + }, + "seed": 1544894267, + "version": 234, + "versionNonce": 63933787, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.0530047510710574, + 179.03128371089633 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "YWHB-Nw2VxgFSkL-pcYdK", + "focus": -0.12996340741750428, + "gap": 11.08333333333303 + }, + "endBinding": { + "elementId": "htYeX7jml0k3AqG6W3Q_x", + "focus": -0.1090092112744266, + "gap": 22.666666666666515 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "htYeX7jml0k3AqG6W3Q_x", + "type": "image", + "x": 635.4793956043954, + "y": 3506.039682539683, + "width": 1024, + "height": 473, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b2f2bb", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ao", + "roundness": null, + "seed": 1128147605, + "version": 156, + "versionNonce": 748760533, + "isDeleted": false, + "boundElements": [ + { + "id": "j-ZCuk6tMAZF7uiWTtIk4", + "type": "arrow" + } + ], + "updated": 1764879496527, + "link": null, + "locked": false, + "status": "pending", + "fileId": "7828d57068931cdd86458a8cbc891ed6ee122d38", + "scale": [ + 1, + 1 + ], + "crop": null + }, + { + "id": "a9YgxexRbVJcxPwYgreZn", + "type": "arrow", + "x": 1147.7941516061107, + "y": 4019.4978280481223, + "width": 2.5974080402759228, + "height": 247.43565838777295, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b01", + "roundness": { + "type": 2 + }, + "seed": 1067879003, + "version": 921, + "versionNonce": 230655093, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "elej2TV9eJ1qTHpBpx-2H" + } + ], + "updated": 1764879496527, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.3147560017152955, + 139.37773758213234 + ], + [ + 2.2826520385606273, + 247.43565838777295 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "elej2TV9eJ1qTHpBpx-2H", + "type": "text", + "x": 1036.3792216542001, + "y": 4122.625565630255, + "width": 167.20034790039062, + "height": 40, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b01G", + "roundness": null, + "seed": 553249339, + "version": 35, + "versionNonce": 359281307, + "isDeleted": false, + "boundElements": null, + "updated": 1764875186373, + "link": null, + "locked": false, + "text": "/anonymizer/anonymi\nze-document", + "fontSize": 16, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "a9YgxexRbVJcxPwYgreZn", + "originalText": "/anonymizer/anonymize-document", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "h9PsSXNcnOUBkMSzBjg6z", + "type": "rectangle", + "x": 1052.940182148664, + "y": 4308.408197109689, + "width": 215.66666666666663, + "height": 280.0447761194029, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "SQGLLXLdvVhMNPoAROpAt" + ], + "frameId": null, + "index": "b08", + "roundness": { + "type": 3 + }, + "seed": 1300012667, + "version": 142, + "versionNonce": 1041008603, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false + }, + { + "id": "UieOrbihjMKgv6n7yxaDj", + "type": "line", + "x": 1096.3954060292608, + "y": 4348.64451551765, + "width": 94.95771144278605, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "SQGLLXLdvVhMNPoAROpAt" + ], + "frameId": null, + "index": "b09", + "roundness": { + "type": 2 + }, + "seed": 720967451, + "version": 110, + "versionNonce": 1216664699, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 94.95771144278605, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "10owweBOlc7qCyP6aRJJV", + "type": "line", + "x": 1092.0153180830398, + "y": 4375.792065073328, + "width": 48.28358208955214, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "SQGLLXLdvVhMNPoAROpAt" + ], + "frameId": null, + "index": "b0A", + "roundness": { + "type": 2 + }, + "seed": 1604156347, + "version": 213, + "versionNonce": 1133153563, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 48.28358208955214, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "BQmhv6TG0JxjRxuwDfSMw", + "type": "line", + "x": 1092.561879853752, + "y": 4405.564643767066, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "SQGLLXLdvVhMNPoAROpAt" + ], + "frameId": null, + "index": "b0B", + "roundness": { + "type": 2 + }, + "seed": 395070555, + "version": 243, + "versionNonce": 311439803, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "4Hm0xXA1DdypSLAR9BoaN", + "type": "text", + "x": 929.1926219227548, + "y": 4635.127102582326, + "width": 436.57354736328125, + "height": 40.2363184079602, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "SQGLLXLdvVhMNPoAROpAt" + ], + "frameId": null, + "index": "b0C", + "roundness": null, + "seed": 1452804347, + "version": 162, + "versionNonce": 112738907, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false, + "text": "documento_anonimizado.odt", + "fontSize": 32.189054726368155, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "documento_anonimizado.odt", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "5LyI3ky8p3RebAfVu1tA_", + "type": "line", + "x": 1091.5670478203056, + "y": 4433.945510542525, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "SQGLLXLdvVhMNPoAROpAt" + ], + "frameId": null, + "index": "b0D", + "roundness": { + "type": 2 + }, + "seed": 37958043, + "version": 141, + "versionNonce": 685063931, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "OSeMl_IH1Pq_pZxGzfw_4", + "type": "line", + "x": 1098.7315886444599, + "y": 4460.844384416316, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "SQGLLXLdvVhMNPoAROpAt" + ], + "frameId": null, + "index": "b0E", + "roundness": { + "type": 2 + }, + "seed": 734021179, + "version": 175, + "versionNonce": 1130997659, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "b4ihNsBPXcvY1lhr6-Ohh", + "type": "line", + "x": 1099.084131656272, + "y": 4486.3503160727, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "SQGLLXLdvVhMNPoAROpAt" + ], + "frameId": null, + "index": "b0F", + "roundness": { + "type": 2 + }, + "seed": 435275483, + "version": 281, + "versionNonce": 749206587, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "mq_eSKsQgMs7MFsq2AJNk", + "type": "line", + "x": 1098.0892996228258, + "y": 4514.731182848159, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "SQGLLXLdvVhMNPoAROpAt" + ], + "frameId": null, + "index": "b0G", + "roundness": { + "type": 2 + }, + "seed": 325727099, + "version": 179, + "versionNonce": 1349696731, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "YN6yT1TNVtadNJ1X5Melr", + "type": "line", + "x": 1105.2538404469801, + "y": 4541.630056721949, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "SQGLLXLdvVhMNPoAROpAt" + ], + "frameId": null, + "index": "b0H", + "roundness": { + "type": 2 + }, + "seed": 1751911451, + "version": 213, + "versionNonce": 2052876667, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "9G3g0qab8TQWTTo68WKEc", + "type": "text", + "x": 1536.304106606377, + "y": 329.64285714285705, + "width": 476.1529541015625, + "height": 283.3333333333333, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0L", + "roundness": null, + "seed": 915516597, + "version": 11, + "versionNonce": 672956187, + "isDeleted": false, + "boundElements": null, + "updated": 1764875143575, + "link": null, + "locked": false, + "text": "{\n \"document\": \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n \"labels\": [\n {\n \"text\": \"R., MATIAS EZEQUIEL\",\n \"start_char\": 0,\n \"end_char\": 23,\n \"attrs\": {\n \"aymurai_label\": \"PER\",\n ...\n }\n }\n ]\n}", + "fontSize": 16.19047619047619, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "{\n \"document\": \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n \"labels\": [\n {\n \"text\": \"R., MATIAS EZEQUIEL\",\n \"start_char\": 0,\n \"end_char\": 23,\n \"attrs\": {\n \"aymurai_label\": \"PER\",\n ...\n }\n }\n ]\n}", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "RCTW7-Twmc0vzPbjJZlva", + "type": "rectangle", + "x": 629.1746031746031, + "y": 2613.596856218215, + "width": 1090.833333333333, + "height": 576.5587918015109, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0N", + "roundness": { + "type": 3 + }, + "seed": 2097245493, + "version": 450, + "versionNonce": 492327899, + "isDeleted": false, + "boundElements": [ + { + "id": "3VriZd7FuEOwMpdPbnIiX", + "type": "arrow" + } + ], + "updated": 1764875205688, + "link": null, + "locked": false + }, + { + "id": "Hx_F-eFq9Elf3olyEAh5H", + "type": "text", + "x": 791.7764842274424, + "y": 2644.4357759284935, + "width": 711.4058227539062, + "height": 516.4285714285714, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0O", + "roundness": null, + "seed": 1957523003, + "version": 210, + "versionNonce": 1767565467, + "isDeleted": false, + "boundElements": [ + { + "id": "3VriZd7FuEOwMpdPbnIiX", + "type": "arrow" + } + ], + "updated": 1764875208055, + "link": null, + "locked": false, + "text": "[\n {\n \"document\": \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n \"labels\": [\n {\n \"text\": \"R., MATIAS EZEQUIEL\",\n \"start_char\": 0,\n \"end_char\": 23,\n \"attrs\": {\n \"aymurai_label\": \"PER\",\n \"aymurai_label_suffix\": 1,\n \"canonical_entity_id\": ,\n ...\n }\n }\n ]\n }\n]", + "fontSize": 22.952380952380953, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "[\n {\n \"document\": \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n \"labels\": [\n {\n \"text\": \"R., MATIAS EZEQUIEL\",\n \"start_char\": 0,\n \"end_char\": 23,\n \"attrs\": {\n \"aymurai_label\": \"PER\",\n \"aymurai_label_suffix\": 1,\n \"canonical_entity_id\": ,\n ...\n }\n }\n ]\n }\n]", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "M3_9cSeevpvgvuGOJmtLu", + "type": "rectangle", + "x": 341.0753968253973, + "y": -946.5918028903099, + "width": 215.66666666666663, + "height": 280.0447761194029, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0P", + "roundness": { + "type": 3 + }, + "seed": 630737269, + "version": 231, + "versionNonce": 857423573, + "isDeleted": false, + "boundElements": [ + { + "id": "EOtxLYz_c5dBLBZTsf6bP", + "type": "arrow" + } + ], + "updated": 1764875361089, + "link": null, + "locked": false + }, + { + "id": "e0VYlO1HBUE6Y61hMmjQS", + "type": "line", + "x": 384.5306207059946, + "y": -906.3554844823498, + "width": 94.95771144278605, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0Q", + "roundness": { + "type": 2 + }, + "seed": 924406485, + "version": 197, + "versionNonce": 1258245525, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 94.95771144278605, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "zz4q6sGFYLSQEGm4_Z_pZ", + "type": "line", + "x": 380.15053275977357, + "y": -879.2079349266717, + "width": 48.28358208955214, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0R", + "roundness": { + "type": 2 + }, + "seed": 2054303797, + "version": 300, + "versionNonce": 2102055669, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 48.28358208955214, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "S9t8FU85SfKbOpC8RR_gM", + "type": "line", + "x": 380.6970945304854, + "y": -849.4353562329334, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0S", + "roundness": { + "type": 2 + }, + "seed": 227041685, + "version": 330, + "versionNonce": 1522445397, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "DL-iPq6d9xxkjVSnghr83", + "type": "text", + "x": 394.1873371239051, + "y": -619.8728974176731, + "width": 82.85454631444826, + "height": 80.47263681592038, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0T", + "roundness": null, + "seed": 898721525, + "version": 211, + "versionNonce": 53500341, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "text": ".docx\n.pdf", + "fontSize": 32.189054726368155, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": ".docx\n.pdf", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "eb7-zegRUYIJqYobn0GTg", + "type": "line", + "x": 379.7022624970393, + "y": -821.0544894574741, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0U", + "roundness": { + "type": 2 + }, + "seed": 1964163157, + "version": 228, + "versionNonce": 2143640341, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "76_13jw7FNEf9CD3vN4K7", + "type": "line", + "x": 386.86680332119363, + "y": -794.1556155836837, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0V", + "roundness": { + "type": 2 + }, + "seed": 1642196405, + "version": 262, + "versionNonce": 418590837, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "7bpvHSILjOEGoxBVuVvmL", + "type": "line", + "x": 387.21934633300566, + "y": -768.6496839272999, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0W", + "roundness": { + "type": 2 + }, + "seed": 992243477, + "version": 368, + "versionNonce": 1820285397, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "hunvhsGsscndVNFWE5PbJ", + "type": "line", + "x": 386.2245142995596, + "y": -740.2688171518405, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0X", + "roundness": { + "type": 2 + }, + "seed": 1062192245, + "version": 266, + "versionNonce": 1132418869, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "T8wwR2IwhMmplgY6InC65", + "type": "line", + "x": 393.3890551237139, + "y": -713.3699432780502, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0Y", + "roundness": { + "type": 2 + }, + "seed": 1110011349, + "version": 300, + "versionNonce": 676898965, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "EOtxLYz_c5dBLBZTsf6bP", + "type": "arrow", + "x": 560.3079384765335, + "y": -801.9441674997673, + "width": 311.1087281901333, + "height": 3.0858479987831515, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0Z", + "roundness": { + "type": 2 + }, + "seed": 46590773, + "version": 818, + "versionNonce": 1436837659, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "AQOsTPNecUVIP5dcYG-CL" + } + ], + "updated": 1764875651472, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 311.1087281901333, + 3.0858479987831515 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "M3_9cSeevpvgvuGOJmtLu", + "focus": 0.024877658056798795, + "gap": 5.80129262390335 + }, + "endBinding": { + "elementId": "vWBP3oXNVHZGwRqV71dye", + "focus": 0.16647678467610177, + "gap": 26.273809523809405 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "AQOsTPNecUVIP5dcYG-CL", + "type": "text", + "x": 617.1124170125182, + "y": -797.1021624155233, + "width": 164.99977111816406, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0a", + "roundness": null, + "seed": 1099807893, + "version": 47, + "versionNonce": 156055285, + "isDeleted": false, + "boundElements": [], + "updated": 1764875337505, + "link": null, + "locked": false, + "text": "/misc/document-\nextract", + "fontSize": 20, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "EOtxLYz_c5dBLBZTsf6bP", + "originalText": "/misc/document-extract", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "vWBP3oXNVHZGwRqV71dye", + "type": "text", + "x": 897.6904761904764, + "y": -891.8596681096677, + "width": 536.5656565656566, + "height": 232.7272727272728, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0b", + "roundness": null, + "seed": 372466165, + "version": 414, + "versionNonce": 1404522139, + "isDeleted": false, + "boundElements": [ + { + "id": "EOtxLYz_c5dBLBZTsf6bP", + "type": "arrow" + } + ], + "updated": 1764875606006, + "link": null, + "locked": false, + "text": "{\n \"document_id\": ,\n \"document\": [\n \"R., MATIAS EZEQUIEL SOBRE\n128 1 párr\",\n ...\n ]\n}", + "fontSize": 23.27272727272728, + "fontFamily": 8, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "{\n \"document_id\": ,\n \"document\": [\n \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n ...\n ]\n}", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "id": "AJupGiIgf295eDXNF1NTl", + "type": "rectangle", + "x": 1911.6785714285718, + "y": -959.9404761904758, + "width": 521.6666666666664, + "height": 428.88888888888886, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0e", + "roundness": { + "type": 3 + }, + "seed": 807684629, + "version": 367, + "versionNonce": 1386791739, + "isDeleted": false, + "boundElements": [ + { + "id": "hotXOq7-8juh39kLJem4p", + "type": "arrow" + } + ], + "updated": 1764875606006, + "link": null, + "locked": false + }, + { + "id": "uIFK9kOYNJE4xD48qvRxR", + "type": "text", + "x": 2083.5527161189484, + "y": -757.9960317460313, + "width": 127.13986206054688, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0f", + "roundness": null, + "seed": 1570520949, + "version": 331, + "versionNonce": 2091546331, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false, + "text": "NER outputs", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "NER outputs", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "InHz3D1JmEtzd_JC59uqm", + "type": "text", + "x": 1934.4354277111238, + "y": -887.162698412698, + "width": 476.1529541015625, + "height": 283.3333333333333, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0g", + "roundness": null, + "seed": 1999538389, + "version": 169, + "versionNonce": 660586261, + "isDeleted": false, + "boundElements": [], + "updated": 1764875581978, + "link": null, + "locked": false, + "text": "{\n \"document\": \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n \"labels\": [\n {\n \"text\": \"R., MATIAS EZEQUIEL\",\n \"start_char\": 0,\n \"end_char\": 23,\n \"attrs\": {\n \"aymurai_label\": \"PER\",\n ...\n }\n }\n ]\n}", + "fontSize": 16.19047619047619, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "{\n \"document\": \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n \"labels\": [\n {\n \"text\": \"R., MATIAS EZEQUIEL\",\n \"start_char\": 0,\n \"end_char\": 23,\n \"attrs\": {\n \"aymurai_label\": \"PER\",\n ...\n }\n }\n ]\n}", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "OKZB0eDL-ABRXZHWhREn2", + "type": "image", + "x": 2663.960317460318, + "y": -976.9960317460313, + "width": 1024, + "height": 473, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b2f2bb", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0h", + "roundness": null, + "seed": 1317474869, + "version": 341, + "versionNonce": 543979707, + "isDeleted": false, + "boundElements": [ + { + "id": "hotXOq7-8juh39kLJem4p", + "type": "arrow" + } + ], + "updated": 1764875603696, + "link": null, + "locked": false, + "status": "pending", + "fileId": "7828d57068931cdd86458a8cbc891ed6ee122d38", + "scale": [ + 1, + 1 + ], + "crop": null + }, + { + "id": "lhZO6MPRRTWy3V55I0vZM", + "type": "arrow", + "x": 3756.6202534941517, + "y": -758.447755452392, + "width": 403.8000183105464, + "height": 5.908527289127619, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0i", + "roundness": { + "type": 2 + }, + "seed": 280036245, + "version": 1548, + "versionNonce": 1898165275, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "GHolMdYEXJBOSjiLEJF7i" + } + ], + "updated": 1764875603696, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 195.91698365823413, + -2.048276293639333 + ], + [ + 403.8000183105464, + -5.908527289127619 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "GHolMdYEXJBOSjiLEJF7i", + "type": "text", + "x": 3760.5369624941827, + "y": -745.4960317460313, + "width": 264.00054931640625, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0j", + "roundness": null, + "seed": 1331022069, + "version": 47, + "versionNonce": 2111430683, + "isDeleted": false, + "boundElements": [], + "updated": 1764875595533, + "link": null, + "locked": false, + "text": "/anonymizer/anonymize-document", + "fontSize": 16, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "lhZO6MPRRTWy3V55I0vZM", + "originalText": "/anonymizer/anonymize-document", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "H45OT-nnyrYBIdX_fHnB-", + "type": "rectangle", + "x": 4251.421104004587, + "y": -923.9736436863299, + "width": 215.66666666666663, + "height": 280.0447761194029, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "heHx6O1UXsEtwJkRBikSl" + ], + "frameId": null, + "index": "b0k", + "roundness": { + "type": 3 + }, + "seed": 1543168597, + "version": 364, + "versionNonce": 1947781947, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false + }, + { + "id": "HkLFBBIeoT2NmdHolQogn", + "type": "line", + "x": 4294.876327885184, + "y": -883.7373252783694, + "width": 94.95771144278605, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "heHx6O1UXsEtwJkRBikSl" + ], + "frameId": null, + "index": "b0l", + "roundness": { + "type": 2 + }, + "seed": 997576629, + "version": 330, + "versionNonce": 787590107, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 94.95771144278605, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "dP23wQQAJGa8EbTko6TLk", + "type": "line", + "x": 4290.496239938962, + "y": -856.5897757226916, + "width": 48.28358208955214, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "heHx6O1UXsEtwJkRBikSl" + ], + "frameId": null, + "index": "b0m", + "roundness": { + "type": 2 + }, + "seed": 1005326613, + "version": 433, + "versionNonce": 690321531, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 48.28358208955214, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "AzAxanw4nR0bo9R-OtYUW", + "type": "line", + "x": 4291.042801709675, + "y": -826.8171970289535, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "heHx6O1UXsEtwJkRBikSl" + ], + "frameId": null, + "index": "b0n", + "roundness": { + "type": 2 + }, + "seed": 1255351925, + "version": 463, + "versionNonce": 276214043, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "cPvNsqjYFBYhf1dvqCfUM", + "type": "text", + "x": 4127.673543778677, + "y": -597.254738213693, + "width": 436.57354736328125, + "height": 40.2363184079602, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "heHx6O1UXsEtwJkRBikSl" + ], + "frameId": null, + "index": "b0o", + "roundness": null, + "seed": 183537621, + "version": 382, + "versionNonce": 911726011, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false, + "text": "documento_anonimizado.odt", + "fontSize": 32.189054726368155, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "documento_anonimizado.odt", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "FZMsE2ftxJlqlXy5y1Kot", + "type": "line", + "x": 4290.047969676229, + "y": -798.4363302534942, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "heHx6O1UXsEtwJkRBikSl" + ], + "frameId": null, + "index": "b0p", + "roundness": { + "type": 2 + }, + "seed": 710528309, + "version": 361, + "versionNonce": 434857563, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "c5LayQF2_U6-IqmzQigBO", + "type": "line", + "x": 4297.212510500382, + "y": -771.5374563797035, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "heHx6O1UXsEtwJkRBikSl" + ], + "frameId": null, + "index": "b0q", + "roundness": { + "type": 2 + }, + "seed": 2020892309, + "version": 395, + "versionNonce": 839115515, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "CZP0a-w2xzB-aIeBIdTfj", + "type": "line", + "x": 4297.565053512195, + "y": -746.0315247233196, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "heHx6O1UXsEtwJkRBikSl" + ], + "frameId": null, + "index": "b0r", + "roundness": { + "type": 2 + }, + "seed": 1005962229, + "version": 501, + "versionNonce": 1998834587, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "YGpIXuZRgXAXrbeyTS72v", + "type": "line", + "x": 4296.570221478749, + "y": -717.6506579478604, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "heHx6O1UXsEtwJkRBikSl" + ], + "frameId": null, + "index": "b0s", + "roundness": { + "type": 2 + }, + "seed": 1222868309, + "version": 399, + "versionNonce": 763207739, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "ABZSKgAIt_rm_gemO06m9", + "type": "line", + "x": 4303.734762302903, + "y": -690.7517840740705, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "heHx6O1UXsEtwJkRBikSl" + ], + "frameId": null, + "index": "b0t", + "roundness": { + "type": 2 + }, + "seed": 1452477109, + "version": 433, + "versionNonce": 1861476571, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "hotXOq7-8juh39kLJem4p", + "type": "arrow", + "x": 2444.591377430666, + "y": -762.2243671485597, + "width": 196.3689400296521, + "height": 3.692569021604527, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0u", + "roundness": { + "type": 2 + }, + "seed": 755633851, + "version": 323, + "versionNonce": 1071716059, + "isDeleted": false, + "boundElements": null, + "updated": 1764875655966, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 196.3689400296521, + -3.692569021604527 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "AJupGiIgf295eDXNF1NTl", + "focus": 0.03289811846175117, + "gap": 14.626683917308128 + }, + "endBinding": { + "elementId": "OKZB0eDL-ABRXZHWhREn2", + "focus": 0.06765961386393643, + "gap": 23 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "gXsFYxTEKv14jMJGLJI8O", + "type": "arrow", + "x": 1452.931012958739, + "y": -762.3018491588567, + "width": 403.8000183105464, + "height": 5.908527289127619, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0v", + "roundness": { + "type": 2 + }, + "seed": 1974610101, + "version": 1579, + "versionNonce": 919034907, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "Q4IQFcmT8i1SnTx5DuzTS" + } + ], + "updated": 1764875655966, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 195.91698365823413, + -2.048276293639333 + ], + [ + 403.8000183105464, + -5.908527289127619 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "Q4IQFcmT8i1SnTx5DuzTS", + "type": "text", + "x": 1566.9144893334446, + "y": -809.3501254524961, + "width": 167.20034790039062, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0w", + "roundness": null, + "seed": 2134198805, + "version": 55, + "versionNonce": 1592755067, + "isDeleted": false, + "boundElements": [], + "updated": 1764875655966, + "link": null, + "locked": false, + "text": "/anonymizer/predict", + "fontSize": 16, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "gXsFYxTEKv14jMJGLJI8O", + "originalText": "/anonymizer/predict", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "jFnC8PtgWHvjc9weWuFWX", + "type": "text", + "x": 326.69940776522253, + "y": -1119.4404761904757, + "width": 654.3769721137143, + "height": 76.99999999999989, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0z", + "roundness": null, + "seed": 29900859, + "version": 85, + "versionNonce": 988358325, + "isDeleted": false, + "boundElements": null, + "updated": 1764875700204, + "link": null, + "locked": false, + "text": "Anonymization pipeline", + "fontSize": 61.59999999999989, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Anonymization pipeline", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "hdfluh6k2tthpQT1_UP4m", + "type": "text", + "x": 326.69940776522253, + "y": -3.9404761904756924, + "width": 1170.5897216796875, + "height": 76.99999999999986, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b10", + "roundness": null, + "seed": 2091673109, + "version": 149, + "versionNonce": 725184187, + "isDeleted": false, + "boundElements": [], + "updated": 1764875707681, + "link": null, + "locked": false, + "text": "Anonymization + disambiguation pipeline", + "fontSize": 61.59999999999989, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Anonymization + disambiguation pipeline", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "appState": { + "gridSize": 20, + "gridStep": 5, + "gridModeEnabled": false, + "viewBackgroundColor": "#ffffff" + }, + "files": { + "7828d57068931cdd86458a8cbc891ed6ee122d38": { + "mimeType": "image/png", + "id": "7828d57068931cdd86458a8cbc891ed6ee122d38", + "dataURL": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAHZCAYAAAAYFEnDAAAAAXNSR0IArs4c6QAAIABJREFUeF7snQd4FcXXxt+9JZUUQhJC6BhaKCqgVAUEQSxIL6KoIKAiKEqRooQiRRAUQSlW9K8IAgICChYEAREsIERASuiQhJDe7r2733cmd8Nm2ZvckISWs8/DE5K7d3fmN7Ozc94554wEPpgAE2ACTIAJMAEmwASYABNgAkyACTCBW56AdMvXkCvIBJgAE2ACTIAJMAEmwASYABNgAkyACYAFAO4ETIAJMAEmwASYABNgAkyACTABJsAESgEBFgBKQSNzFZkAE2ACTIAJMAEmwASYABNgAkyACbAAwH2ACTABJsAEmAATYAJMgAkwASbABJhAKSDAAkApaGSuIhNgAkyACTABJsAEmAATYAJMgAkwARYAuA8wASbABJgAE2ACTIAJMAEmwASYABMoBQRYACgFjcxVZAJMgAkwASbABJgAE2ACTIAJMAEmwAIA9wEmwASYABNgAkyACTABJsAEmAATYAKlgAALAKWgkbmKTIAJMAEmwASYABNgAkyACTABJsAEWADgPsAEmAATYAJMgAkwASbABJgAE2ACTKAUEGABoBQ0MleRCTABJsAEmAATYAJMgAkwASbABJgACwDcB5gAE2ACTIAJMAEmwASYABNgAkyACZQCAiwAlIJG5ioyASbABJgAE2ACTIAJMAEmwASYABNgAYD7ABNgAkyACTABJsAEmAATYAJMgAkwgVJAgAWAUtDIXEUmwASYABNgAkyACTABJsAEmAATYAIsAHAfYAJMgAkwASbABJgAE2ACTIAJMAEmUAoIsABQChqZq8gEmAATYAJMgAkwASbABJgAE2ACTIAFAO4DTIAJMAEmwASYABNgAkyACTABJsAESgEBFgBKQSNzFZkAE2ACTIAJMAEmwASYABNgAkyACbAAwH2ACTABJsAEmAATYAJMgAkwASbABJhAKSDAAkApaGSuIhNgAkyACTABJsAEmAATYAJMgAkwARYAuA8wASbABJgAE2ACTIAJMAEmwASYABMoBQRYACgFjcxVZAJMgAkwASbABJgAE2ACTIAJMAEmcDMIACajZlIUBZJ0ufj0Ox3q3wr6nM7Vf0d/H/UaUVFRmDhx4hXX1t9DvSaVQb32pEmT8nxXf462Dtr7G5WfrkVl0X6m/p9+qvdSOajl1nKh/9Pftf+091W/Y1TuwjwuRmVU761lqZZDvba+PvS5vnwqA3299OVTr62y0ddbvbYrnur11PZUf7pi46rOrvqVtg+66rtaHvr+7qrvqNdV+4uesb4/69mrv+vPM+Jp9Ay4atP8niMt6/yeTaO+6265jPqYEUNX7WhUV+01C9Mf82s7/TXVdjQqv1G/Ucce9TN9H9K3j3ZM0v9fPybp72fUbvoxOD9uRmOKftzV1iO/8VTbD4zawtWzZDS+6Pugqzpox1213kbPl5a5UR/Wlk3fbq7GtfzasaD3oRF3/bjhqm0K08/1fVfbRq6ePaM+rY7V2neH/nlwNa4Z9Wl9X3BnvpDfs6HtL1RnOozecwXNGYiJ0XitvvOM5gB0L1djq8pN+x519Xy5M19Q76UtY0HPlb6/u2Kg7ytq+xr1c/0YqX1HGz0v1BYF9VstR7UNjeYa+fUV7djrzrilPcdV+VzdL7/z8xv/Chpz9X1XP5YYPXdqG7l6Pgtir3/nuprrqtfR91VXfVdtU+1Pbf2N+m5Bcxp3+6N+vHA1L9I+U9qyGT1jRm2n/b7RXNeovkZjmdE7Q/scuLpOQTy08wF9P9C3u3oPd+ZH+n6ofX6172L1GXY1f3b1Dtc/+676hZFtmN9cVzeHUZzXzTFgrzwkV/NR3amuvu+qy4i/31QCgApVb+iqNdS+7PQTDbWRtQ+L2nCuXrgFdXijSZ3+YdafY3RPo5e0tnNrHxqjshY0yKnX13dgI17qwJDfy5jOUV+o+gda39tc1U0dbLT3yW8gUa9D9zUqm6sJuv7+2smPtq20EySjOmg/1/YtdYA0agP9xEf7kjQy6vQvPW2dtNfS9veC+q5RvfR10ZZd+xIo6NoF9RXty8jVM1oQN1eDvP572kHeaMDUczB6KbhbRu1kUt/GrkZb7QvBHa6u+q2Wub682nvr+6hRmxt9X/uc6V+irq7hahwxaiNXz5zRs68yo+9oX5r6ZyG/8U1tZ70YZvQ8a/9m1HeNnkf9s6itn35yaTQeqOcbjYf6+2nHXaMJUkH9SstTa2gasTfqS0bXN5rwu3oHFjQW6e+pv7a+T7saO/TXUbkZTWaN+rSrd4T23eMOM/V8oz6v/76rsT6/PmPEUx0f9P1Dez9X/UT7HOsnxUbl0D53dF9Xk3b9mJKfgeXqPvqxQD/m6Puc0dijv7b2HKOxX/9suxr/CvOsuDKotG1iNFfNb6zObyxTr+tqrmv0vlJZq2OEfs6VX9/VltNorqZtN32b6cdA7fOulkXfZto21Y+l2j6p7y/59V398+/uuGvUv9Qy6ccOo77navxz9bzq207P1qgeRv1Ifx0jO8GoDEbPh56zvi+76seu3q36vmjU1/XXdDXuGvU9d+wxoz5mxMxI7DZ6F7oam5z9X3GOo6pBL2x0/VzC1TivKZdbgsBNIQDoX156gEaDd34dVtsoUVFRpi1btlwxDtLf2rRpA/1POtHoM/UC9B31HH0ZtOeo16Dz1Y6p/S6dq95f/b/+3vQ9uo62PPp7qNdWz1PLpL+2eg1t2bWc6XyjsurrTdfXctOWWQuZ7mdUFn3bGpVTy0P9XNuGKkf1ftq6af+mL7tRuY3aUs/iis6j+YO+P6jtoK2DlpHKWFsHtW6u+qO2r+i5qtfWllF/HT0HI0bae2jrlF9buyq3vj/r+anXVPu3vg2Myq8vh7beRs+REXNXdTQqr/5Z1TLUPifqd7XtqraRti/o+4NRnd3tu9pnMD+22pdpQf1DP5Zp20i9n1pH7bjk6v76/q3vU/pnN7++pG8fd8Zd/XihHyO099OWRTuJ0PYrV2XQ92V9PfR9TuWgfScYjQna/uZqzNU+P9rnwah91L/px1NX7aQfV9wZc/XPpP6ZVdtN+1y4O/nV89e2jVHfNRofjMZcbfvQ//XvXG2f17aZ0ZxC/8zox109e+3n+vez0XihHx9cjZPaZ9KoP+rfy/r+4mrM15ZfZWnUd9XxT20D/bNmNJ8xYq9lrG8X/fipHYfdGXf1fPXtqa2fWl5X466rdnc15hr1TX1f0P6uH4eMyqp952jZ6OePRs9xfvfWczZ6L+r7gtGzajSeaPuBqz6ofT61Y7Na5sL0Xe370NWcQf/sGD0LRuNufnNdbf8xeubV9jQatws77qrPkcpbfT71baKtZ0H/17Mymt8Yja1aJq7GLv0YqR8H9ddwNe4a9S/ttfVzIVdjnP7+rsZYV+OFvp49e/YUhnq9evXET7UcGoNfFQRym0EVyrTtYuBlcesIAAV1wCJ+Lhfx+/x1JsAEmAATYAJMgAkwASbABJgAE2AC7hCgRXhy8zepK/89e/bEihUrFKe4oYYI5An9pgvnFxpg4GVlKAhcDw8ANaZfdla6HAA/AF4AhDHu4eEhwHl6elIlxfmpqam5MMuUKSN+V3+qH9Dv6qE9n/6m/Y7ueoY5BtxpOfUcKm92dnbuV/S/F+Za2mvS/9Xraq9ZmOurLPXXye8a6meuzinM/Quqe0Hl0DJQ+4aetatz3C2nvr6FLZNRHV0xdLdMBXHTf55f/7iaPlDY+xfH+fo6qO3qqvzFcc+SuoY7Y0Jx9gWVkZaZ9jnRjqv6v7vD4GrHH3euXZRzjJ6zonI1qmthxwR9nzUau7T1LuieRtdz9X2j99K1aL/CjKN6Htr6FbX98utPxX3tgsZ5V/3T6N1e3GVz97kyGnf148j1KptRP8lv/Lqe467RPMRVG1zNGJzfPMNoLMhvzChoPCls3ylq/3D1fVfjVn7PVX7vQFfzpvzuo29Xo/HVXV6FGZuM+rKrshTH/V31r2t5T60NR/fV23H0N63dZ2QDGn2npPgUw3VtAJIaNmyYsm/fvjTn9cguF3ZpVFSUpPNOEIa8UyQQp0dFRQmxQBe2JYQDOnQhBXmKfK0FALLsqcJkqbcnL3cArQHUJbsfgN0AaJEN9AIaqaSvXwx9hC/BBJgAE2ACTIAJMAEmwASYABNgArcAAbJ5YwD8DmA1gE0AUpz5+TzbtGlD9rII83Yeitb4X758OXr16pUbPqANg9Hl0BHnaEQB8fu1FgDofvUAvAqgFwDrLdCAXAUmwASYABNgAkyACTABJsAEmAATYALuECCvdxEG8P92cSbZ+gBmAfipefPm3p6enjZd3oErPADIrldvpBUAtB4BdI4uSet1EQBo1X8+gNpOTwCzG4R4hd4NSHwKE2ACTIAJMAEmwASYABNgAkzgZiFQqVKlQhc1NjY2T+h1oS9w437hIoBRAJaSMKBL8pnrAUCeAHSsWLFCDQkw9ARQwwO01ZUk6ZoLAC0ArAQQqvE8uNYeCDduk3PJmAATYAJMgAkwASbABJgAE2ACBRCgVV19Mjj1KyaTSeRR69atGy5evIjvvvsuz9W8vb1RvXp1vPzyy7jttttABvVXX32F77//HpmZmZBlWcSP0086LBYL7PbLUdrqvd1tJG1Z1TJTDH+TJk2QmJiIjIyMKy5llPFePalKlSr47bffkJJCHvPuHVQHh8OR6wrv3rcKf5Y2K7/2/2azWdw/n4OSyamh8k/+f3j8V5QOzxkKYNmyZQt9WdHttqENC7gi2Z9+S1fnva+pAFAdAPW+mk7jn3oUr+wXvl/xN5gAE2ACTIAJMAEmwASYABNgAiBjXzXUyci88847cfz4cTz44IP4+OOPxf/btm2L06dPC0O+R48eoH3ryfDfvn07zp07h9DQULRs2RLp6elYuHAhpk2blmtch4SEiFVmMrj37t0rrv/KK69g//79mDdvnjgvLCwMU6ZMQVpaGj744APx2bBhw4TI8N5774ky3HPPPUJo+Pfff0HlpPsHBgaiT58+uP3225GVlZWnNdU6GTUxlY8EBBIsCjqoDP379weJHsnJyQgKCsLPP/+M9evX5/tVVbSgsoaHh2P8+PHw8/MT9aPvOxPVC2GEDG06Z/jw4fjkk09w+PBh1KlTBx07dsTKlStx4sQJUecCBABteUgRaQZgH+kvzn9kuOeGARw7dkz5448/xHfatGkj/k4hA7S9IO0koF5MvxWueo1rtQI/zRn3T/dj47+g3sqfMwEmwASYABNgAkyACTABJsAEDAhoV+XJkKaV9NatW4vVfkoQt3HjRnz55Zc4dOgQ2rdvLwz9GjVq4NtvvxWG+jvvvCPc6OnvtWvXFuIAfU5eASQYkDFJRu3s2bNx3333CQN43bp1wu2cjPhq1aoJQ3/kyJFCNCAjmK5B33nuueeEIEDeByQa7Nq1S4gKr732Gg4cOCCMZzqvXLly2LRpkxAC9Icubj3Px9OnT8fatWvFtejIzxOABIB7770Xd911F7p27SpEi7i4OPz66684evQoqlatKoz4U6dOCRGEGGzYsAHx8fG54gpxpPoRt5o1a2LQoEG4++67Ub58ecydOxdWq1VwItGDxIatW7cKoYDEkn79+gmhopACAFXrZwDd27RpkxITE2OJiYmhhXOyoXON+06dOlE7k1eArOYLcCYNzD2HtgQgwUezo8A18wCoBWCPc6s/ymjIif94KGMCTIAJMAEmwASYABNgAkyACVwlATKSmzZtKlb0v/76axw5cgQ//PADfv/9d0ydOlUY+2Ssd+jQQXgA0Ko5rdaTgU8G7DPPPCNWxX18fPDPP/9g8uTJOHnyJC5duiRW7clophh9CiUgQYGM4JdeegnNmzcX/8jIf+utt4Th+8gjj6BBgwZCWHjzzTfRvXt3cW0ytMkQjo6OxmeffSZq6uXlJYznsmXLCmO7QoUKeUIM6JyCBADtCj55JxR0kAcCGcHt2rXD0KFDUb9+fSFUjBo1Smw/T2LCgAEDhKfAhQsX8NRTT4n60qo98bz//vuFoU9eFbTyfvDgQSGcEPvnn38ed9xxhzDyieM333wjPhs7dixmzJghRJmrEADIZqaE+d9qvAAoDkOJiIggXlJMTIziFAFyYjUANG7cmMqX+zv9jQSh6OhoNU/ANRMAnnAmM6BCU9K/a+V1UFBf4M+ZABNgAkyACTABJsAEmAATYAI3DYEuXboIw5IMTDJKKX6fDGIyyMkYJtd+EgDIaCdDlQQAWuFWjwceeACzZs3K/ZyEATKKP/zwQ2G8q4caXkAGJIkJFCIwePBgkNFJQgJ5BKxatQq1atUSxjP9JE+BH3/8EWfPnkXDhg2FEU0r/OQ5QOEE9BkZ3NdaACDvCBI46Cdtk1e3bl0RfkBeCVSeevXqCcHi1VdfFfUijwXyMiAB4N1330XlypWxe/dusaK/Z88esdJPBwkJVGe6JrGhes+ZM0d4N5DnAHkcELurEADIUF84ePDgFxcvXuzbsGFD2759+8jzwhETQ7sHQiIRZufOnSQCKOQJ4Gw3ESpAuQLoX1RUlELtRweJABMnTrxmAsCXAChdIRn+HPd/0wwvXFAmwASYABNgAkyACTABJsAEbiQC5KZPK+9k2JExSmIAGeIzZ84URurOnTuFsetKAKBVbpvNJoSBzz//XJxHYgKtfKux+GoMfHBwMJYsWSI8DMgYptVu8iAg7wByfyeh4cUXXxReAuQ+//bbb4tykNt7o0aNMHr0aCFOUMI/Eg3IqF62bJlwpff39xe/Uw4C/VGQB4AaAuBuIkC9AEAx+o899hhef/11EbpA9SEXf6on1YN4kvcEHRTuQKIJhQVQbgXypiBXezLq6TPKiUDtQTkAyJOgb9++wrth9erVIjSABIXCCgBU/4iIiF0NGjTosWrVqninF4CpfPnyjgsXLpART3a1MOYpBMDb25tEAPpVady4sZofII8ooDIWiRivQYc+CCDCufp/DW7Ht2ACTIAJMAEmwASYABNgAkyACdx6BEgAoBV8Wvkn93QyxMntnAzvHTt2COOUVvJdCQBEhFb3KTcAGeO0Ok8r4ZSkTz3IYKVEfLR6TwLA5s2bhbG+Zs0a8Tu5xJ8/f14Y//S3//77T8Tik+s/Gdu0mk7u8rT6TbkGKAyADG3KOUAr6pGRkeLalF+AwhG0uwxQGVQBgspABiuVl8pE8ft0PxIw3EkCqNZHKwA8+eSTeOGFF4SAQt4Mv/zyixA3yAuAQhgo6SGt6tOOCHRQeUkcoJwBtPJPoRO08k8HCTC9e/cWdXzooYfEdzp37oyAgIBcr4yrCQGgPAkdO3Y82rp16wdfeeWVw2XLlg2g0AxnHgA5MjJScrr1K82bN1fIE0CTH0D8Xw0H0G4beC2TAJ4BEH7rPX5cIybABJgAE2ACTIAJMAEmwASYwLUjQAn0KJs/xZuTAECu5xSXTsn2Bg4cKLwAKEGfq20AtQY2ZeMnt3WjrfjURIN0HQohIGOXXOcp8d+xY8dEojsyyCmGn8IPKERg6dKlokyUMZ8MYlr9pr9TTD2tilPCQDKmyQuBsv/T/8n4p3tpD3W7QDL81S0JyUinc+nalNzQ3dV/uq6aDPDTTz8Vt6EM/S1atMCff/4p8h5QjgKqZ6tWrUSWf/IEUA91JwEKYSCxhAQDKpewxmVZhGGQIEFCBokU5IFBeQ5IDKG2oF0BCusB4OR3vmXLlo8MGzZsb0REhPeRI0fIsKcbU1i9uoivxvsLo98ZDkBFU0MBRDjAgQMHaHcAERJAuRCKywNAex1Juy8lYDoNoILrxyJPnoJr9/TwnZgAE2ACTIAJMAEmwASYABNgAkzgmhMgw5nyD/j6+gqDmgx8dbXfqDCqsU0r4eRhcCsfvr5+6Nz50fONGzfpPHLk4r01amR6HzuWqAQGyiar1WqPi1OE7d2wYbi8b1+809XfQ65WDYiJsSiRkXcq3t5llcaNgcOHw5UtWw6Ic5Yv74levVYUnwCgMfp1ogILALdyB+W6MQEmwASYABNgAkyACTABJsAEmEDxEPD29iWPhPMNG9756Jgx8/Y1aODnffr0afnSJdkEWO2A7LS3zTJgcgoAVuequpfzZ4A8eHBjLF78B9q0eUT5/5yAiIraAiAkRz0ohuOqPQDyS/JQDOXiSzABJsAEmAATYAJMgAkwASbABJgAE7gpCFAIAQkA9eo17DJu3Nx/6tTx9z548Jzs7+8nJSenOQCy4SUFMMshIZISF0dbAFaRPTw8lJ07zymdOtVVTpwoo0RHHwAQqnO3LyEB4DLZKAQHv1dACMBN0Q5cSCbABJgAE2ACTIAJMAEmwASYABNgAiVO4NFHH70QGVmnyyuvvPlPWJifz/nzsQ4/P0Uym80OSqAYE5NIKQGcHgAkBpAngFmpVs1Dztkp0Mdp+HuLhIB+fhQKsAXLlz9fMh4AKhFKMrB06f9OA4rLHADq/gQlTpFvwASYABNgAkyACTABJsAEmAATYAJM4AYmYJIk3H9/hwuRkbW7TJ48+YDZbPa+cCHdQcn/AgJMDtpl4OTJZCUoKFhOSEhUgoPLIT4+USYBIKdaHjJgUQAP8Xvjxq1y8wG0adO6ZHMAuCMAyMWmQdzArchFYwJMgAkwASbABJgAE2ACTIAJMAEmUAABk6SgQ4eOF2rXjug6ZcqUAxaLxTs2NkO4/vv7mxw5ORAlpVq1skpMTLLTC4DyAdBBIgB5A5AAYFF69rxTWbHimPACoKSA4eHhxWZ95+YAUJSca1JsPwsA3L+ZABNgAkyACTABJsAEmAATYAJMgAm4R8AkKUqHDh1ja9as1W3q1CghAGRkZDhSUyH5+wfac3ZBEG7/svOncvlnXgGA7ti4cYAMNBbbRQJ+xS8AaPYldEsAcBRxF0DaMoL+0dYQtMcibSlBP+l3+ntBB53LBxNgAkyACTABJsAEmAATYAJMgAkwgetNwEgAiIsjDwDy3pccOcb+ZQEgKCgIFApQvrxZvnDhSgEAIAFAPYpHAKAVf4ovECv/tB3gpEmTpKioKDkyMtIjI9MRQzkA6O/6g/7mMMmw22SkSzIa+PkhyGGCwwPYlZYCxSbD32yFzarAIytnwwOLyQoHsiGZzUhMl+DpkYUmEWXg62nCsXOZOHgqA2W9vWA1W5FpV6CYsiApJlgsJkh2CSZIcCQo8A7xhH9tb2RlpiP5WAaUdAnmADPs2TJgNYl6yIoCS7FFSVzvrsT3ZwJMgAkwASbABJgAE2ACTIAJMIEblQAtYFstJuEBUKNGRPfRoycdCA62eMfHZzp8fRUpLc3kAFJMTvubDGwy7pUaNWrIycnJcnx8PP1N+0983rhxY8XPz09p06ZNsVi3eQQApwggSZLklgBAZU40yXi+cmX0qxgGm7cZ3rIJxy4lY+LRY4hOTkV1iwdSHTIcEuBhMiPTJiM504Tq4V5Y+EJFhAVa4WGWkJhmxxc/x2HB+vPwsXjA02qGA3aYLBKQBZhMJpgyJVR4uByqdgsVuokDNmTEZuPEV7G4uCsVkq8EyWoSQoBkBsxm4ssHE2ACTIAJMAEmwASYABNgAkyACTCBkiOgFQCqVave49VXpxwIDrZ6x8dnOHx9IaWlkQdAqmp/KxUrVlTMZrN88uRJYeirggD9v1KlSsrp06eVyMhIOTo62rkbQDF7ANCquTP2320PgAtyJsZFRODVuhGg+tAqvzfMsMsO7E9MxlO79uGSww5fRRI1spjNSMkAgst64fNXy+P2agGiBXLNdMmM1z89gVkrz6BGeV+kZdtgUkyQTApssTIiBlRA1T7lYTGZhSDgULJBaQsy4rLwx4tHkZVig9XfCrsikzrABxNgAkyACTABJsAEmAATYAJMgAkwgRIncKUAMDk6KMjik5CQZffxUaT0dPIASKVy5FnlJw+AxMREJSEhIXfF/48//hDndOrUSTlx4oRCIgD9npu8rwi1KZIHQMUAT2xudjeyzUCWBbA48xhSeawO4O1DxzHl+FFUVKyitGazhKQ0CeMeD8FLXUORnnk5tIAKYpYsuJQi46GJ+3H4TDZC/SxCHpAzHPD298Tts2qgTJgXkClOFqv8ikmBZAVOb7iI6BmnYS5ncsonOYIGH0yACTABJsAEmAATYAJMgAkwASbABEqSwGUBoENclSrVeowbN/VAUFAZ34SERLuPD6R0iptH2hUu/tWqVZNjYmJUL4A8ngA6saB4QwBUDwBnPgC3QgAeLB+EN5rUE+796kH/JeOffv6QcBHP79gHL7NJnGPLBny8rFg0IgztG5VDRhYlP5SFOz8dZskEi2RB92mH8P0fiQgLtIgcALaLdoS3K4eaz1SEZ6BVJBSgDRIoakKxSDB5A8kxmdje5wAsoWYoZql45JGS7CF8bSbABJgAE2ACTIAJMAEmwASYABO4JQhoBYBKlar0nDDhjQNly/r5XrqUaPf2VqSMDBIA0rWGvmrsXxECoA8JcP5e/AIAkacEAGRau5ME8OHQIExtUg82c86OheoOhvR/+rfq3Hm89Hc0AiRzzqq8LMHL04JZQ8LwUNMgZNicfvqSDJNsgtViEYn+SADY9GcSwgMsUBwmKCkOhNwViDovVYJnWQ/AJsEiOWP9PUwweUtIOp6G7f3+hTnUBMlsgkzCAsUk8MEEmAATYAJMgAkwASbABJgAE2ACTKAECZAAYLFYlA4dOsRVrRLea9y46QcCA8v4JiaSAIArBIDw8HD57NmzSpUqVQzzAGiTBfbs2RMrVqwoegiAoihk71/1LgDeJmBNm6Yo7+2BLNkBBySQqe8pS2I1f+qBw3jr9ElUlnNCAChuPzZJwcg+5TDxiTCkZZKLv5yzmg/Aw2zCmYsyHnr9AE7H2xDoLUFymCHJCizeZtwxtQYCavoCWSaIYttMIgTA5G3G8RUXcOi905AC6IoKZEkRIgEfTIAJMAEmwASYABNgAkyACTABJsAESpLsrDvhAAAgAElEQVSAVgCoVLFi79dee8MpACSRAADVAyAkJARxcXHaVX8jDwBZTQRIQkDPnj2VYhEAnO7+uQIAAYmKinI7CWCiko0+5Svgncb1IFtyjG1ZlmF3yNh8LhYjow/BYZPFdnwUAuBtkXD+koQKwd5YMiIMLRr4i+9IwpffhOwsCc+//x++2pqAUD8vQLaTagDJJkFJkxHWOgh1R1QW2wDakh0wy2aYy5iReDwNe4b9h6wMOyQfwA4Z1AC06wAfTIAJMAEmwASYABNgAkyACTABJsAESpKAkQBQvrxvmQsXkp0CABm9GUax/vQ38ffg4GDtdoD6cIGihwCoHgDOnyoPt0MAaMX+ZHoGHgwLwZCIKqjk7YUUuwM/XbiIBUdi8jXAU1NsmPpUNTSrZ4Gfl4z/zmXivXWp+OHPNISVE0WAQyIPgZxwArHgn2SHbzVP3DawPMpU9UZWmh3J+9NxamUcsuJtMHuYIHlIIvkf5TSQi+4kUZJ9hK/NBJgAE2ACTIAJMAEmwASYABNgArcAAa0AUL5ChT6TJ07fHxrq4xcbm2LL8QDIEQCCg4PJM16OjY3V5gAw9AKoVq2aEhMTo3oAFF0A0HsAOBMBui0ASBYJ2bKCJJtNNJmn2YQ0hx02h4JQL094Kfm74MecVRDkn3NOQrINfmVMKFeGcgEosMsKHE4XflUAMFkk2BPtyI63wbuiB7IyHZAzFHiFeoA+o/LTP7EfYXHtk3ALdEauAhNgAkyACTABJsAEmAATYAJMgAmUHAEDAeBAaKhPGY0AoDgFAIrDVygMwM/PT05JSck1/l15AFAIAJW8ODLciW0AyQNAXDBn27xCCQD5InTm+HN1jgS75iMTefuDiqI4DXiHUz9QBQC1cGrOADuH+JdcD+YrMwEmwASYABNgAkyACTABJsAEmIBbBFwLABQCICkZGeTi75vHxd8pAOTZDSAyMlKOjo7Osx1gZGSkEh0dXXQBQJsE8Gq2ASyIhFNQcHmapFCMPgke6uHMBkg7JABXeADoBYCsAkL8zc7LFVRO/pwJMAEmwASYABNgAkyACTABJsAEmMDVEnAhAPjFxibbVAHAIAdAnmSAQUFBSkJCgly+fHnlwoULshoCQIv2jRs3LpYA91wPAI2x7r4HQI7HAB9MgAkwASbABJgAE2ACTIAJMAEmwARKLQGtAFAhrHzfqKiZzhwAeQWA4OBgJT4+Xqzwqx4AAQEBclJSUm4yQOcWgPokgEUXAIrqAVDQCn+pbX2uOBNgAkyACTABJsAEmAATYAJMgAmUGgIaASC2Qlj5x1wJAE7jPtft38/PT3HmAdAKAHl2C6DVfwJZHMvvwgOArqWGADhFATkyMtIjI9MRAygVRGI93aEJGSg1jcoVZQJMgAkwASbABJgAE2ACTIAJMAEmoCfgjgCgzwGgbv9HHgC0M8ClS5eE4R8eHi6fPXtWiYiIkI8cOULGOIUAFF0A0G8DWOhdADgEgHs+E2ACTIAJMAEmwASYABNgAkyACZRyAu4IAGoOgJCQELELgFMAyJMHwJnwz2hbwKILAEXeBpAFgFLezbn6TIAJMAEmwASYABNgAkyACTABJlAYAUAfBuDv76+QB0BiYmKu4R8eHq6cPXtW/H7D7ALAOQC4ozMBJsAEmAATYAJMgAkwASbABJhAaSdQGAHA6QGQmwfASABQEwFGREQozjCAonsA6EMAqNE4B0Bp77pcfybABJgAE2ACTIAJMAEmwASYABMoDAF3BQB1F4DQ0FCx6n/+/HlZKwDUrl1b8fT0lPft26cKBCIHQM+ePYsuAOhDAKiCUVFRUlRUFCcBLExr87lMgAkwASbABJgAE2ACTIAJMAEmUGoJuCsAaNz/3fIAKNYQAKMcAJMmTWIBoNR2W644E2ACTIAJMAEmwASYABNgAkyACRSWAAkAZrNZ6dixo7vbAN58AoDZbC4sFz7/FiEgXbkz5C1SM64GE2ACTIAJMAEmwASYABNgAkygcARkWc4RAB5oHxsWVv6xmTNn7vfx8fGLjU22eXtLSkYGFN0uANdeAFBzAJAngHMLQKqllLMdQaRHueCLMYqCCoWr+o16NiVQLL6DILl7KDC5e2qJnMfGeolg5YsyASbABJgAE2ACTIAJMAEmwARyCVitVqXTgx1jq1at/Njs2bMNBQA1B4Ca5I9s74J2AXCGARRfDgCnEKAWXAgAUVFRHiazZwzgWgAwma6vYSt0CjeOwhjrblzuxjhFKSR7yT1WN0bluBRMgAkwASbABJgAE2ACTIAJMIGbi4DD4VDq1Kkbm5SU8NigQSP2h4YaewC4sw2gRiAovm0A1RwAWgFATQK4f/9+j3r16uUrANxczcGlZQJMgAkwASbABJgAE2ACTIAJMAEmUDIElJwjdsuWLY+1a9duf+XKlf3i4uJskkQhABl5svqTgR8WFnbFLgBawz/HM59CB4p5FwBVAKAwADUJIAsAJdMp+KpMgAkwASbABJgAE2ACTIAJMAEmcOsRcFcACAkJUUgUiI2NJQPfMARAFQI6deqkbNy4seS2AVRzACiK4gGAPQBuvX7JNWICTIAJMAEmwASYABNgAkyACTCBYibgrgBQmBCAiIgIxcPDQ4mOji56DgBtEkCquzMRoMgBwB4Axdwb+HJMgAkwASbABJgAE2ACTIAJMAEmcMsS0AsAoaGhfrGxsTZvb+88IQDkARAXFydCANLS0mTyBjCZTHJiYqJcu3Zt5dChQ6rrf24IAEErjtx2dA2KKdBeiwWAW7ZLcsWYABNgAkyACTABJsAEmAATYAJMoCQIuPIAyMzMFHH8tNBe0C4ARgJAZGRk8XgAqEkAWQAoiebnazIBJsAEmAATYAJMgAkwASbABJhAaSFQHAKAURLAYhMA1BAA+ilcCiQJvAtAaemeXE8mwASYABNgAkyACTABJsAEmAATKC4CxZEDwFUIQM+ePTkEoLgaiq/DBJgAE2ACTIAJMAEmwASYABNgAkygKATyywFAK/sZGRm5Mf2hoaFiFwA/P788OQC0HgDVqlWTLRaLcuTIEQofKFkBgHcBKErT83eZABNgAkyACTABJsAEmAATYAJMoDQRMPIAOHXqlEgCqBcANIa+NuGfPvlfySQB1IYAqNsA8i4Apamrcl2ZABNgAkyACTABJsAEmAATYAJMoCgEiioAREREyLTa37BhQ3nfvn0iaaC6ZWCxhwA4twDkHABFaXH+LhNgAkyACTABJsAEmAATYAJMgAmUSgKFEQDUEAAy8v39/ZXk5GQy9nM9AMLDw5WzZ8+WrABArSRRJkBAZg+AUtlnudJMgAkwASbABJgAE2ACTIAJMAEmcBUECiMAFDYEoFg9AJy7AYgq8i4AV9HS/BUmwASYABNgAkyACTABJsAEmAATKNUErlYAMPIA0G0HWLxJAI1yAHASwFLdd7nyTIAJMAEmwASYABNgAkyACTABJlAIAu4IAMHBwUp8fLxICuh0+TdMAqiGAERERIhdAIrVA4A8/zkHQCFalk9lAkyACTABJsAEmAATYAJMgAkwASagIeCOAKAm9dMKANfMA8Dp+k/lpLh/iv8XPzgHAPdjJsAEmAATYAJMgAkwASbABJgAE2AC7hO4WgHAhTeA8BDo1KmTsnHjxuLxAFAFANUDwCkCsADgfhvzmUyACTCBUklABKJpDiEf88EEmAATYAK3KAHyUNYfpvzrqn9ROM9WpLzXklDAdW5RolytW5NAUQUAdRtA7W4AJA5ERkYq0dHRYqW+qAddgx5P7bVYACgqVf4+E2ACTOAWJ+AA8kzZxEtEO9krjjdUPgxdzCtzv1HCt7/FW5erxwSYABMoOQIsIJccW77y9SfgjgBglANAGwJQu3Zt5dChQ7l5AYrVA8Bp+AsBgHIA0KFuA8hJAK9/B+ISMAEmwARuGgJGFjlb4TdN83FBmQATYALXigALANeKNN/nehBwRwBQcwCEhoYqsbGxZOgbJgEs0V0AOAng9egefE8mwASYwM1FwGZzIDUlCxnpNkiKBLPVjNBQ35xKuDL2S0oEkIHsDBsSk7KQZbPB02qFXxkrvL08AIuU1z3hGmBOSkpCYmIiUlNTQYJ6uXLlEBQUBE9Pz2tw99JzC1qroH92e7bIW6T+M5lMah6j0gODa1pqCFy6dAnp6emw2WywWq0oU6YMfH19YbFYrhmDzMyi30p2vg+sZsBspkXH/F8fRb8jX4EJXHsChREACtoFQP1c3QUgv+mW2zXV5gBQvxQVFSVFRUXJ+/fv96hXr14MgApuX5BPZAJMgAkwgVuCgKwAJgmQZSDuQiq27TyB06eScfxEAtLTbMiUZQQGeiEspAwqV/RH7VrBaHx7RVgsEmyyDCt9uQizO300gdipRpEQH5eKnX+cwamTiTh5NglxCRmwO2RYLWb4lfFArWpB4l+zllXg6+sBqofwcCNj0aRAgiRkdnORW0lGdrYdP27egaNHTiMuNhmX4rPhYQmELJtg9kpG+fK+CA71QZMmdyOyXnVIJgccdrPAYr528/Yi1/RGuIDdLsPhcCArKwvpaZk52YplWfyjgwwhLy8veHt7w2o1Qyp6A98I1eYylEYCwi9XRtIlGatWrkP0v/vw378XkZycCrtNgX+AL6pUDUOVquXR4PaaaN6iEQIC/IVBLY5iFl3J8I85CWRk0mCa/8UL+Di3NU1mBSYTEB4moVzZYi9yaew1XOcbiIC7AkBISIgSFxd3xTaA2hwA6jaAJZIDwCkEqOjEe5VDAG6gnsRFYQJMgAlcawIycOZsMlZ/dwibth7DsZOJgM0BL4sZnh4WwJFjfGXIMsxmM7x8LahfNxT9uzZEy2ZVxCotebRJNMu7mkmpMzuNmqQmLT0bn3z5N7bvPImTp5OQnpYNi9UEi0kCbWRjlx1QZAkWswSLxYTKEUF4tF1NPHJ/Lfj65azCCzGA5scmBaYizpLXfLMF33+/E7Zsf9zb+n5Uv626uIdC0oIkwyIryM6wY/fvW/Hnn78isl5NdOt5HyIbhAOKiWe8bvZn6mNk+JPxQyug2Vl2IehYzOY8AgB5AlA/JCGAfpIHhocXqwBuYubTbgACDocCs1lCcnIi5s39HN98vRXJGf8htHwQygc2R/sOLfHZl3Px266dsJr84JA9EORfAXfcWRdPDnwUffs9CpPJ5lQAiqowkrCWM3b/tc+BnNV/+t1YADBKD2iEVBUIzCZFiBneXkDd2ib4et8ADcBFYALFRMBdAYCmDPoQgMDAQDk4OFg+cuSIKgxof1IiwCLOXi5PyXJzANALlD0Aiqn1+TJMgAkwgZuYwK5fT2LYG5tw5mwqwoN94OtphZmWrmUlZ8XdZBKGmBq0JllMOHkhGV4eZowZ3AI9ut8OnzKazM6FXJWy02RY2MkSzp5JwujpP2HNdwcRWSVIiBCw5J81+lJaJi4lZ+HBNhGY/mpbBAb4wOSZ45WgQLkqd3EyRDMzMzHvzdX49/BFPNK5Oxo0qYS0dBmZmaacxTGTAzJM8CAPCLsDfmXMsGXY8e3KbdizZwcGDWuFhx5pSWvWN3HvuDZFp/5FIRUUWkHGkfDkIPHEjUVOu92OoOCyCAz0B4UH8MEEbjwC9txxQAGFVVmxZ/dBvDZhCqKj92PwoKHo268LakQEw5YtweqRjSf6P4HPP1sBTw9fIYDZ7IBVKgcoZdC7VzfMeXcMQkKtAIpqUecIAGT4kwCQx/gn7zDNI6W69edn8Os/U+iSwosHqB0BhAZf69a5LHBc6zvz/W59Au4KAK48AOjR0CcBVHMGuPP+c4dw7i4Awr1SIwCwB4A7+PgcJsAEmMCtR+D7df9iwlvbYJMVhAf5Istmh2yXyTXMpfFlsZjF6lVqWjYSU7LwTN9aeP7ZDvD00lhrhRABssXUWMG5E0kYP+0n/PHXOVQOLQObQ0G2wwGjSacacUCF9LFaQCLCuUupaHl3Fbwxqi1Cwv3EApbsUGCiPAGFPOLi4jB76lfw8q+LHj3awSY7YLOZ4XAAksWWY5wq5tyyKSYHzJIJkpKNcmU9ceJIAr5c+hXue6A6+j3xQCHvXrpOJwNem1MhN6mDmwIA0SJPgDL+vkIEoPkNH0zgxiJAq/VkrNORhV9/OYqn+k7EyfO/Ysu2b9GiRWMxXqVnJMGebYVfgBV9+/bFyhXrhaglK3bISrYQSR/r/QJijqQgOzsLy1bORbUa5YtY1csCwL+HFacHgJS700tWFpWdhrzcBOKQSLB15uXIDblSf89JMp5bJkkBHHaAnMmqVilZAeD8uYu4lJCCmOPnEBebgJDQIFSsHITwiuUQHHzNlYcitgt//WYg4K4AoDHqr0gCqBcA1BwAxeIBoM0BoBcAOAfAzdDFbq4y0urZ5YPchtUX381VD21pyR6ilxi9iMnd2WSywEzJqIQ6ntcpzuHIOwGVJIqB45Wpm7f1b62S2x05a9IH/jqHIeM2IisjVUyOsrOzhQu2ulOMWmuawKnunCKmXmdgpSdloU+fhhj6Qgt4CLf7Qrq924FLcemY+OYW7Pn7LLx9LKIcdBgZc/ry6VvnrvoVMW5kawSGeYvwBFqgz//IeX5phcpkciAtS8HrY99DGZ870efJ1riUpPs2zY01iQtM8uXxzmQGsjJt8C3jheSkDMx980s82b86uvZsAQke18U41ZbVmEP+Gy2WtEGddCkVycnJuW1eUGu5+lwVAQIC/Hi8vVqI/L0SJ/DPvoPo3+8l/L3/T3h5peHgwWhUrlw5T5+lFf8BAwbg888/v6I8H330Ebp06YKHO74oxurvf1oIHx9SX6/ycMZeZWXK+OegCVmZDlEWT08J69dvxvFDhwwvHBYWhvbt28MzMCDfG9P8R5FNMMGB6tXMCAu9ynLm87WkxDR8/dXP+PSDDSJ3yEOPNkfZoDLIyMjAxrX7YP1/zn0eb4/OnR9C2XI+QsDmgwkUB4HiFgCqVasmWywWhcICevbsySEAxdFIfI1rSEA3n3TIOfFuN+9BRj8dWiOeVgDVFU99VFxeY59e5iwA3Lytf8uVXAFiz6Vh5OTNOLD/AkJD/Fwa/9q6q71cLwDYs+2QsxyYOOl+dGhfEyQYFOqtZQemzPkFy7/5F+X8PGiz2tznRTX2CzL6teVMS7ejd8+GGDmsudOoL6gFVQFAFjH97767Eru2Z+C1KQOQcCkDklnvYpv3eTdBFjkJtAeV19PLgovxyZgzbRwWfTQKVatWLaggJfL5jSwA0AQ9IT5JZDwvjoNEgJDyQSJJII+7xUGUr1EcBEjQpL5J+S2eHTAN36z9HypUzYJs98WHHy5B3bp1hThIIizNFWhXkaFDh2LVqlXi/9pj2LBheO655/D333+j2yOjMODpfnhj1nNXX8x8BIDu3fuidauWuPfee0VeDu3h4+ODevXqwV7A4kZJCwAxMScxbPAcBAYGYMTox1CrdlV4eHjAbFFACUVpF5GD/x7HjKlLkJScgAXvT0fV6mHw8LAIIZcdhq6+6/A3ReJhOmK3bNnyWLt27fZXrlzZ79SpUzZvb296spSMjAw1elL87lwxzLMNoNYD4JoIAM4pGu8CwD24eAk4jf/VqzZhz+6/4OXlg9AwPwwa/BToRVDSq0nFW5mcqznkdJhNPli7cjf2/PEbfMoAaSkK+vTphXq308qp7HyZKDj47wl8s2YFEhISQOrd3Xc3EStb9PLngwncCARkOzB74U4sX7Uf5Xy9RFI9o5V/tax6eUsvAFg8TUhNyESdGsGYPL0DqlQIKJQA8Nv2U3jspdWoGFwGnpIwp3O9EApj+OeW16EgLsuG96Z1QtuW1dwoy+Ua7t93HNNmrMHoV19GWkYWJPHc5h/Db0KOQKiKAJR4kP5PRq1/gBeOHjiL06d/wgsvPn5ddgS4kQWA2NhYpKdmFfBY5C+w6r/sU8Ybodc+0PhGeLS5DDcgARrDaHyluc+6b7ZjyLNPY+mny1Czrj+yMsxY9tVnWLZsmdjVgg5Kbklbjb788svo3LkzaFtA7bF161bMnz9fbA34z5/pKOdfHet/nIs7GtW8utq7EADIs/Gxxx7H+PFj0ahRQ8NrZ2VRiFb+3o2XBQAF1atJxeoBQC7/j3R4Ee07NsWUGUPEIo3ZbIHsXHSi8CKLhSpoFfO4saNn4ufvTuPbH2ahfFheYeXq4PG3SjsBdwUAygEgSZISGxsrQgD8/f2V5OTk3HCAhg0byvv27cuTBLBYPQC0uwBwEsDS3m1LqP4KkHgpC9269sUvW3fADE/4B1qxfuPXaNrsjhK66bW5LNVp7Teb4VcmGGnpiXhrzlQMG/4MbHabyEZOL/g1q7ahT78HQS+emTNn4uWXX2IB4No0D9/FTQLHj1zCqGk/4PjxSwj08oBDudLtX3spmwmg8E9a2Re5oXVLJvS5t8mEjIQMjBh5L3p0re+G0X35Dj0GrMC/B+MRVtYbDjvtJuDczs95irrvO62O0YoxHTRZplUekZzQuT2cekXaO/vAiYvo1TUSb0/oeDn01iWfnBhYmz0Dnyz+CSlZgbj3vkbItuWUx6xLQigpNmcWeppsAhkZqofQZRGAbkXhAFS+7EwJO39ajYe7NkG9BpUKFBTcbEa3T7tRBQBy1T1//jwUR0HhUYUTAIg5eQHQ/ul8MIHrTUAdn9LTM9G5wyg0aBSEt+dNAaVVpbFt9uzZGDNmjBgr6B+NbfRsLFq0CM8888wV49vy5ctFfgD6rtglQ2mJQc90xoIlo66uqgUIAC++/CJatWoGMvaNDpMzV4erm1PuANq9hd4h1asBYaHF5w3as/M4RNSsjCkzB+V6aNJcjBKKHj92FlWqhoBCgtSwTLNZwZgRS/DfoZP4cvVkeHre/OGpV9fo/K3iIlCQAODr6yvHx8erq//CwPfz85NTUlIU2gUgMTFR6w2QRwCgMhbH05KbBJAu6MwDwNsAFlcP4OsIAjabA1argg3fbsOIoXPFftk+3n6IvZSA/v3b4YNPJsMuZ8Buk5CdpYgJvMWqCMVbPVJTc9zM7DYZPr5euRN8b28THA4T0lKzQAm6aLscPz8/OhMpyXacPx+LsArlRCyc2WQGeZQ65Gwoig2y3Ru+lKXcudc57W2uKA54eErw9DTBYbciPSMNXp4+4nFLvJSBlJQUVKkaCsmcCbPJCzt37Ebfbq8j6ZJNlDs17SIeeKgZvlw5S7yIKcSBDKPv1v+OJ/qMwaXUfzF77ji89BILAPx43DgEaDL67beHMW7mFgT5eYgV92yaoDkon73TyDeZRPb/LLtDJNdr1CgMJ08l4tLFTHhaTVeEs5AwQP0//lI6enWqi5Gj2sLDm65WcPblmKMJaNV1KWpXDYJscwoRIobg8mExWZCUZUOj28NxZ53yuBiXim07T4okhDTZgzmvgahI9DyacPpiKtZ/3Ac165QroAFyykleO/Nnb0bT+zrAL5AmjQ5A9oQ+hQlNGU+dOotDhw6JZ75p07vh5UsJAL2gQE1aaILJbIciW2E127F72y4EBCajX//7DQQAGceOnoG3txdCQgPEeKiuGh4+dArh4eXh4+sBi8UDNnuayApORv2a1d/j3tbNkZaWif37jorxiMY1Mh68vT1x6tQFHP3vNFq3bZpv/e12R66ASSf+sfsQkpNS0bZ9Y2Rn20V7/7n7sBA1724eKX7XakBX60JLvCn5nyk3OZqrYtpBnhle3h6o37A6MoVHpeuD+ngZf/ICKIFg4xvnUeaS3CQEFGd3/eeff/HgA92x4bvVqN+gdm52/RkzZmDs2LG5XoL0/JPYuXjxYgwcOPCKnCyUF4DyA6iHw2HBPc0exfJVCxBWQU1051xYVCwFWxBFFAAsEm3JCWzf/jtiYmKE56P2KCkB4KfNe/Dtmh2YM3/4FT3h5x9348Xn3sInX45Ho8YNcvOLkCcmicidO4zBg4+0wIjRvXShnTdJp+Ji3jAEChIA3AkB0IQGXFsBgJMA3jD96KYvCC3EZWen481pH2PqlI9Rs0Y11Kx5G777/kdERFTEt99/gGo1QhBz7AI++OAjnDp1CnVqNcKY8U/BZFJE5vH353+JHdt/Q4P6TfB4/+74+MMvxET7iSd7wWbLxmdLl+NibIaIpx09oY8w1Je8/y1ijp1BQGAZPPXMw7i/I229BSxa8DV2/fYnKlasiBEjBwqXr4yMLAx74WWkJGWjR6+H0L3ng/hjdzQmT5qJhx7sijp1q+K9eStgtmZi7vxXEVwuXFxr1swFmDDhXTRueBeCyobi1227ULFyIJatfgO33357bngDCQCP9RyJ5IzDLADc9D361qsATSzffud3fLxyL8LL+opVfYeUM0NV3e3pbz4WCx54sCba318TFfx9MHHuL/hx23FUKOsDmyOvAUbnk5tntk1GlYr+eHNKe1SqShPRggWAb1dGI2reNvh5e8AhwuPIFM8rAFzMcqDvw5F4cUBTnDiThJqVA7H+hyOYvnAHvDxMkPIkHYVw26ftA3f/ex7/e6crOnaKyLchHQ5a0bfi8KFj2PjNYTRseR8sVqfurlivcNuP3rcXCxcuFBNJSl7XtHldDH12Arx9c5IO5ob5mc3id6slG0cPxiI7/R/0ebwtPDz0CbtkzJr+OQ79exIfLB0nykriw76/j2L5Fz/i5TGPITgkULSPyUTu8rRnggNN7+yNtxeMRf16DfH3n4dxb1vysKIdHOwwSSbs/fswvlm5Fa9PJtdY10fOdU0iXpYElf8OnUZ8/CXc3ay+EHYoCzhNtmvVqYzbIsiDoegH3fPcuXNiq8WCBICUlCQMG/IWmtxdF088/QACAsoWWADa+SEsLFSItXwwgetJQOSikEyYOH4Rtm89gHWbZwmBTj1IAJgwYULucy+y/suyEAAGDRp0hQeAXgCA4omKoY2w5KNZ6NCpmfOy104AkJwOUBSqMHLkSNx5550YNQudHH0AACAASURBVOpFZGXljOOyuWQ8AGj1/5Eu96D/gPbOdVISnckrAshIz0Zs7EVUqhwiBFU1qSwJAPT/ZZ9vxvZt+zB3wYvw9LzcFtezn/C9b04C7ggAwcHBwv1fGwJglA8gPDxcOXv2rDZnQNE9ANRdAOiniljK8ePkHAA3Z5+7YUt97OhJ9Ok2Fnv2HcALz/YRxnjvrqOQiYt4f8E0DH62JzLSFbzy8jgsWrIUFYPrY/uej1GlajmcPBmHB9o+i0PH/sW0qWMx5Lkn8PADA7Fr99+oGFoDWVkZuJR0FiaJVvl9hJiQlHQJ52LPUm5ymOCJ2jVrYdnqaYisVwU9uw7D199sRJ3qt+P7nxeicpUKOSv7lWojKcWCqZNGY9xrz2PNmnXo3q0Pbqv4MExmB46e2IUaNYPw7bcbULNWFcTHJaFPj2HYsnUXRo0cgiZ33Ylxo97ByZMnMWf+CDw39Amx8qZ6APTq+hLSbcdYALhhe2npLBhNKimR03MvfI+Dx+Lh7+0BMyTYTJcNeuGynmFHxfJ+ePDh2ggv74+OrWvgpcnfY+MPR1E52Bd2Oa+Bri7Yi1VrRca709vijiYUj1qwALBw7g4sXRsNDw9T7pZ6egHgkgLsXT8QX6w+gGmLdmLU4Kbo2qYWer+4CvHn0uDrlddJzqEAZTytOHkuGYP6N8FLw9RJsXG7qzk6fty8AzGHZTRo0QpZZGdLWaDJtVmXAuCjxbNx/PhxLF68APv2/YsBg7ph7qzVqFMvAopMJzsgm+xiPCJS5HaaEJeAY4c2o2fv9gbx6TJWrdiCr/73AxZ98ioCA3Nc159/5k1UrBSKkWP7YdGCb/DXnqNo1aYuOndpi5CQQLS86ynMmTcW9W+vgr1//YcWrRri1MnzmD75E1SqEoIK4UE4czoeY8YPEH//aNF6XDh/EY3uqo3nhnUT90i4mIyPFn+Lg9En8HCXlujS/V4hRMTFJqJZy/o4dzYeS95fhZTkDDS6qxZ69mkHcmUmkSAjIxMrv/oZdevVQP+nHxJeCnS4k/CUvAnOnj0rVjotJtcTcJqc/7DpV7w96yu8PnUAFi9YgwVLxub7AKshKhUrh+XxLiudTz3X+noTIA8Amm0/8uBTaNCgAaa9+UqeIk2bNg2vv/56rpFKohWNSQsWLBAhAPqQK70AYDF7Q8mujOkzXsPLY/peEwHA0/NyTiPVActqBZKTHRgyZIiopyoCyGYpR7wsxhAAiv1/843/oWfftmjesk4eAYAWojIzsoW4GFi2jBiPtAIAvZfi45LRueNILFgyEnc2pu/zwQSujoA7AoC6BWBoaKjIAUAhACQGmEwmEQKg3wZQs2Vg0QUA59NB5RR2P4cAXF1D87fyJ0DK6/p1W9C7y1hkKpn46qvpQgCoFtwfafYDeLjzvfjk0/fhHyhh08Y/MXTwGzh7JhaLPh6Lx5/shJ07/sGjHUeLAfunHfMRViEIT/Wdih827UW5cj6YOvM51KpTFXNnfYY1a36FBV4Y/kpX9H38ASxZ+DUWL/oaFskD6zbNQdv2TdC7y3isXPMT6kfUx4Yt01GxYgWRXKdGtQZISfTDlKkjMGb8M1i7ZgO6dxkMb1N9VKhswiOPtkfDO2rgkS5tUbasHzZt/B29u72C1MxMfPjhZHTr1RZ9uo3E5s27cX+7e7Ho05cRHh6WKwB07zwMWfIJFgD4gbmhCJDRRbGRjz+9HqnpWVAcgKfZhCyLIjwBJCXnJyVQIsM1JS0LfoHe+H7ZY3h1+o9CAKga6ods2kdQc6gCABn/FxMz8PHb9+PuFnVFeE5BCfSmTdiMb7fFwMMrZ0JJyfT1AkC6yYQnO9fH198dxN7DsXhrwv146J7bMGTMOsSeT4NVt8grBACLFfGJGej8YG28Nr5Nge1Ak/TPl65BSkIAmrRtg6xsB6CYAenKBJ6JsfHiepSde+fOnXhrbhQWLvwcwcHlc1L8SsSHQiUk0HVJQMjKSMF3GzfisZ6NUb/hbbryyGKC+kSvSWjXoQkGDumcky386Tfx9OCHcPjgGZw/exEt770dq77+Dnc3a4hnhnRG88b9sPjjKMRdSMO8t1Zg+do38Mj9L+OOxrcJb4Apr32Epi0iMXf+Kxj14gL4lvEUIsE7s5eLz8e+3h8PtRuJOpHV0aHT3Zj6+icY9nIPnD+XgF9++gsr10/DfS1eQLOWddCmXSMseW8N2rS7E737tceD972C2nWroFvPNli1fDuaNq+HF0Z0L5AznUDzD5qcUwJAEgCs5vy3MJs2aQkCAn1F/YcNfgvbdn9U4H3oHmHh5fOstBb4JT6BCZQAARp3aWHhgQ7d8GiXhzF0aN6M/dOnT8ekSZPEVoCUOJQEgCNHjuC9997Ds88+69IDgM6ng3KeHPvPjHFjRmDyjMHFLgCMGJE3BwAtouzatesyKVkSZSTbgvJukCcA5T/q0aMHhg8fjssCgITq1ZRiyQGwc/s/WPHlzxg9vp8I/dQfkyYswbZf/sTqDTPg5xdwhQcAeYK2afo85i16WYyJfDCBqyVQGAHA1S4A+hCAiIgIsQ0glanYcwA4K8oeAFfb4vy9KwgosIkY0bbNn8dve/7CnfXrY+NP7yM4xA+vDH8L7727GhIk/LJrAe66OxJJSRno8cgo/LJtO8aMHYDXJw3H/Hc+x+hR8/HQA/dg9YY5YiI65OnJ+H7TdvR77FEs/GiCiDWbPHEBZrzxIZq3aCD+Vqt2daxZuw09u44EJBt+2rJYJK3p1X08Vq1eh3o1b8eGn95CxYqhSEpKQcRtDZB00Q9T3hiOMeMexzerN6FXz0EwOYLwvxXTxB679DKjCTwp92NGvoF33voazZrdifc+GIvIerchavwSvDltKTw9LVi3+U20uicnzva7Db+gV5fRSLcd1QgAtA0i7wLAj831IyBMdhlIuZQOSrqXnUITTTMctEezPkuccnlCFxxaBv+b3w0TZv6E9T/+h0rlysBGyoHmyBEPAHK7Pno+GR/PexT3tKwKk/PVld/OH9PG/4gNO2Jg8ch5zekFACFYOxRcTLXB4inhuScaY3DfJvh81V68++FueFspt0dejwQhOygSktNteKDdbZgWRS6i+R9kqH/y0UqkJwXhjnvaigR+6jafejx+PkBcbApee+017N7zAwYOHIUnn3zS5Q1MVto1JAWbNn2Dp/rdg9p1qujiTnM8MBa88zXOnIrHtNnP44ulm7Fh3a/46H/jRfx7WHgAAgJ8xd9Pnz6NSW8Mxz13P4V5749DemIiPlm0EyNf74hHO87E7ugF8PP3wYrlG/DPvhi8MrqfWOGnEKjwisEYN3IhYmMTsHTZ6+jXYxJmvTMc4RVDRLhBSGgg/v7zP+zfdwwDn30YS95bi4++GCPKt33rfrw/bxVem/IU3p3zNYa80AUNb78NP/+wF7OmfSHG2NwwkgISA1D4BAkAJHzkFwJAngvDhs7EF19Px57d/2DUS7Ox689lIjSBQhbEBMkgCRkLAAX1eP78WhGQZQrJsaB798ew49c/ENmgkpgrkdFMOTBo9V/NV0HzBPqMRIBPP/0Uv//+u8j2rz5X9BntDHDPPfeIOQr98/Twx7gx09CgYSTmvEOhBAUl1dTVvIAcACNGjECrVneLJIC08h8dfRibNm3KvYhELxYpJxEybaPqawrGJ598gvoNG2DS1CnwLRvg9AAoLgFAxs7t+/Htmq0YNe5xBAb659RZ1CPH6+znH/7Cu+98iPeWTBChQHk9ACDCmlo2HoR3F41E85YsAFyrZ+FWvI87AgCFAGgSAeZm/tdvCViiIQDiXUmrPP//cuZdAG7Frnh96mTLVmD1cODgwWPo2PpFnI2NRavmjfHapMEi5n7LT7/j/XnfIEvJwrRpgzFqLLnMm7DovZUY/sIM1KxeG2s3zcDol+Zjzfq1eGv2axj+8uMaAeAX9H+yMxYsHi9cOie99h5mTPsQzZrVFwJAnbq3Yc3arejZdZRTAFiIVq1a5AoAtapF4vtf3kblymHCfbVSxduQmhiIKW+MwJhxA7B61Vr07PEMZKUevv9uNu7vcFfOxFIC/vvvKJ4bOB2/btuH1q2biElvUFAAtm75GzOnLhUZ1ElEmDjleeG5wALA9emDfNf8CZDJLkFBUnwmBo1Yh/Onk+DlaYFdbISc14CWHQo8PK3IzrIhLNwfH8/pjEmUA2DrcYQG+iAjO++e7aoAQAFmKTY7Fs16GI0bV3SrSeZM/QUrNh2Cp3eOn71eABAJN9OzYPW0YOzQlujQqjo+XxeNhZ/vhiNLgY/VBMVAAPA0mxGfnIneXSIx7pV7CyiLLMajr5dvxMVzPrjz3tYiHwmoQkIgyavBZ2emCqOTDNiYmGOYNfttzJo1C7Vr1za8DyXBykxLx8+/rEL/PveKrNU5k3Q1RCLHkI2LTcJD7Udg9cbpeHPq5wgs64+oNwZj6y87sW3LP0hNtuHP3cfQ5v56eHX8QLRu/iTmLRyF9IQsLP3gVwx/tQNGv/Qpvlo7USTB++vP/di4/neMGNkb363/Df/sPYbU1DT8uOkP3NW0Lt58+wW8NeMLDBnaXayikUFNY9icmV8g5th53Hd/Y+zeFY0Zc3NWFelvE0YvxquvP4EvP9uE54Z1R+UqIVclAFCiQuJHK56uBIAyZbww8PFp2L7jAJ54+mEcjD6Kn3/6FdNmDkfPvvexAODWE8YnXW8CFB4oO8zo3XU01q1fjmzlRJ4i7du3D/Xq1buimP379xfbA9J8nQx/Ouj5fOedd/DCCy/k/o1Egy6dXkblqsGYv3B84ZcMCyEAGLF0SrciDNLLywsrP1uJ7zZvwsixY1C9Rg3QJh/0mUkxF5MHgIzDh07g0w/X48mBD6JW7WpXCAD/7D2Od9/5AOMnPouqVSteIQAkJqbi3ruexZKl49C0ef3r3UX4/jcxgcIIAGoIgDvbAEZGRio0LhSbB4A2B8CkSZOkqKgoWVEUcqCM+f+caRVu4jbgol9HAhRzZTLZMW70O5g163NY4Q2L2RNWiut1btFltynIktPxaOd7sPiTcQgMDMK+v/9Du1bPQ0EWpr45CB8v3oC/9/2Gf/79EbVq35ZHABgwsBveeW+MEACiJizA9OkfoNnd9fH+h+NRv34tfOMUACTY8OMvC3FPqxbo3W0cvl69AZE162Pr7wvh5++Ni/FJqFG9KTLTvTBl2nCMGTsIa1evQY/uAyErDbDp+1m4v0MTULFJLPvxxx/RpeNEkcSHtgMzOcV1+pxUZapf1Wph+OW3D8TOBDkCwMgcD4C3x+GlF2kXAIk9AK5j/+Rb5yyO0JGZasPoSZuxY+cJBAX4IMNmg8Wke8Uokujr584kITjEF5tW9cfAMd9i1YaDaFYvHIo+6Z5TQ8h2OODj74l50x5Anbrl3cL+0YLf8f7/9sDXL8cNXC8A0IQ3MduBof0aYWDfRpgyfxs2/XpchCuYbAokEa6QNykhTZV9Pa04dPISxg9vhUFPN3ZLAPhtxz84Ep2FGrc3gcnsWgCYM/sNBAQpmD1rAnbtOIv+/Z8Ve3a3aWMcamD2AM6ficOxmJ14ul97+AfQK5cEj7wCAI0l5KofUacstvywF69PGQJ//0D06fIaVm6YgtDygfh4yTokJMRj1KtDcM/dAzDvvfFIT0zF4vmbMGFaNzzcYSx+2/sBypbzwYplP+PIkXO4v0NjkU9g489zUK5cAF595T2cOR2HpV+9jnYthuPzFZOFB8D8uStRPqysGHf3/B6NoS/1wIcL12HhJy8LfqoHwPhJ/fG/TzdhwOBHcFtE+FUJADR2kgBAOSnMknGiPhIA5r65DDEn4+Dh4Ykj/53Ent1/YMLEweg/oBMLAG49YXzSjUCA+vvwIe9i0Yfz4MDxQgkANOchwYwOEgPmz58vQgPomuT+f/rkRXR7cBwGDHkQzw57qMCwqyt4uC0AqAnK815B9cDx8pKwbNlqbPhmLV4c+TIa3dUAqZkyzGK7wuIVAKgE97V8DkNf7IHuvdpdIQDs/esoliz6HBOihhh6AOz5/V/Mnv4FZs4diqrVchI988EEroaAOwKAJqZffYi0W/9dkQOgxEIAVA8AZ2gBJwG8mhbn7+gIyEhJScOD7Z/F779Ho1xgBTS8vSZkJVucl50l48h/Z0RsmI+vBcvXTEfb+1oII7tT2+HYsf0PlAsKx7m4M7i9QU3s/HuRSPR3OQRgGwYN7o45744UW2FFTXgX06YvQdMm9fD+B6+h4e21sHbtFvToMkbE7P60ZSFa3XM3enebgJWrN6J8UCVMfGMgIutVx8RxH2D7jj2wyVmYMn0Axrz6LNas+lYIAApIACAPgCZwOHL2Fx/YbzJWrNgM2oqsfsMaYo9eegnTS/m/wzEiWRZlDH9z7ggMeq4LCwD8bNyQBFQBgBbLP1z6J6bO2YoalQKRZbNfIQBkZjgQFu6HfgMbI7xSAJpHhOL0qSTs2XtOGOu2lJznWj1UD4CMbDvuuLMC3hh3H4JC3NuDfccvMXhh9Dr4B/qKy+kFAHIVt/t44491A5GVacPxM8nwMplgkUwYOXUTjhxPQBmn94BaHhIA/H08sPXvM/jhyyfQrGlB3gg5HgCnTiRg4bz1aNu5O8r4ebv0AFj+1Yf46MOluLtJM5w8dRweHv6gON6QkBDDtrd6Adu27ILZIxbPDXwIFgu9+40FgOgDx3H3Hd3QrWcHfL5sBo4cjsUzT0aheat6wk2YYvPJcF70URSeHxSFee+PRXqCHW9NX4FVP05E327jUK5cCKreVhaLF2xE78c7oFffe9Gq8WCMnfikECLXrvpVGBTrNs/G7Glf4MA/J8WWWGtXb8OESU/h4L8n8P36XVi1YbrYt5yE07ua1cWHC7/Fq689gZb3NhCG+UujeqN6DRIA/ipUCIA6B6F6UE6W/HIAkAhAKQI8Pbyw4qtNGD3iLfyxb7kQZzkE4IYcarhQegJCS8zGNyt/wtNPvIrkzP15zti79y+3PQDoi4sWLcLgwYPF/IQE0i0/7cbAgc/g+01rEVGzauH5F0oAEKN03vHf+dtvv+0Qrv/PDh6K1vc1wsUkBYpFgpnSqagCQHUZYSGFDFG4okY595/z5v/Ez5dH97tCAIjefwIvDo3C65OH4p7Wja7wACBBtFxwAKbMoB1SilqewiPnb9w6BAojAGg9AEgUCAwMFEkA1RwA+hAA2lKz2DwAtPkEnAkBWQC4dfrhdayJjI8Wr8OQIVGwSmYMf+kpTJn5HBxyOhwOGT4+Pli94hcMfmoaUtJT8Oq4pzHutSEinmzV11vQu9doeJg8YJdTsezrGeja/QFRlwvnE9C/92T8tPUX9O31KD74bJyIjZs4fh6mTvsYLe5qiHcXv4I77qiPb1b/jB7dRsMEGd//uABt72uGr77YjAFPT0JWdobIP2CCCVWqlEdwSBB+/2MPXn/9BUyIeg5rv1mHXr16wW5vic2bZqH9/TkrhkeOHEOnts/i1OkEtGvfFMtWzYDV6gnyZijj54nN3+3CE70nIjH5DNp2qI2N3y/Dum+2o0+PF5DhOIE5c6Pw0kvkAcA5AK5j5+RbOyd44ocC/P3XWTw7bgNgV+DjYbkiyVRWpozAst6oUz9U/ExNykSgr6fYE37XX2eQmaYTAByAZDUhLjkTzz7ZBM8/0QSSM6ZfwC9gftXmoQ9hz3TAYqYnNOd1R173sklCht0Gq68XWt9VBQ67LMprtZigZMv4fe9ZXLqUkbNarznMVitS0zMRXikQn83rgjJixb3gw2EH5r+9AhWqNEaViBqCi9lMGaTzfpfykPy951+RCCs01B8tWt7j0vhX67Ll5+9Qr64vHnmohTOrtzYE4PKk+lJCGn7+8Q+x3V6D22uIc0+djMWq5VtQsVII7ru/Cf7cc0i47KekpKJ2nWqC2ZH/TuGuZpFIiE/G6q9/gcf/sXce4FFU3/t/t2fTCBBCSeg9FKUINvzZFUFBEBUQpQgCYkNERUpCrwqCUr42QHpXQUUFRBFQEIghEAg1JJAQkpC6deb/P3d3wmYJ6QkpZ54nT0KYnbn3c2925773nPfotbizfTPoDVo0axmEY0eisOvXQyJaqX3H5jj0dyTubN8UTZsF4dtvfkHMpXg83fN+BLduiLNnYpCUmIYOdzUXAufPO/YLzwASUJ/sdjdMJosoUdgiuD58fT1x5XISThw/j4ce7aAYDOcKWxEAyJAyISGB6oTlej4xoFQB8j44c+YMnnjisazwZ8cL1VlO6TRmdP0qVaqgeg2/rEVS3qPPZzCBkiUQG3MVj3V5G5HnfoSkSoYsa0T1k0OHDuUqANBmA81/+jugXf/FixdnmQOSADDo5Xdw8UIcdu5aCY2w5NcVrCMFFgBuvF+Jn5wGgLTBQ20MDPRHRgaVFnW8l9PbZ0kIAMnJKejV7X1RJeWp7vfCanW8Uet0Gly7loqQcV+g872t0HfAw45qLs7PiT/2HMfHcxdj9rwJaNqsdsFTJgpGl8+u4ATyKwDUqFHjlmUAS7QKgGsZQMWQiT0AKvisLOXuLf1sI77b8juqVvPBmA8GiYdLMr+hmtQq6JBwNRnzZq1EzKU41KtfB+99OBB+VX1gMqWhdkBPpKWn4I6WTbHjt5moVdvhbptyPQP/W7wNh/85jvsf6IgRb/YQHzBrVn0vBAWqS02O2Q0bNMSffxzGwgXrxOvGffQK7mjXAmaLDRvW/4olC7chIz0THTq1xKi3eyPqZBy+XLYBw0a8iB7PdcKuX//DzNAlaNCgAd75sBdatGghHiK/2/Y7ftp2ANfT4tDnxSfQu8/jgIvhFC0Mxr//Gc6dvYIqfp4YHzoIiYnJmDNtDa7GJ2Ho8BfwXN97RKRDfkpjlfKQ8e0qCwHnA57orkxpABZM/Ph37Nh5GrWqerktpuh5yFE2yWqVxXe7rILdYhcmT97eupsW9LRsz7DZ4F3FiI8nPIq2bWplPycPCXvZ//7G3GUH0KCmb9ZDnHhwVDnScGQVmRICdpsjKodEDK1aDZ2ajDodZv2uhwQV4pMyMO7tLnjphTuy0nZuOdwufPbuPoq1q/bg5ZFviwgmtUov7u160HOtRi1Dq3W4/Fuy6yE33SYuLh7/Hd+N0W/2hqeR/tt191853XETEgvpvSI348SbbpBd/8jhv90UjJvOKNoOWIHa6rw3jSuNJUUBmDOdhcTz/ffoNiBOAUAxSiN+NWsHwGDQ5UuQyPdt+UQmUAQCNN+pWscH738EnWeymJ9knrd79+4cBQDK81+2bBmqVq2aJQCQF8DcuXNFBAD93Z06dQp3d3oEy5b9D8/2fgQaTQEX/87PBNJdSfj976QaZpPd+bwioV+/l3DDBND9jcb971CB46iAohwlJQBQJMKm9bsxecJXWL9tmjBXVSI3SQQgoz96L6AvBxcJ589ew4ypn+OhRzrjxf6OjSY+mEBRCORXAKCnL/cIAFcTwLZt20phYWFKigB9L/4qAC4uvVwFoCijzq/NIkBvtDfCwtRiZ19ZAZDyK2rAqrUi35POIz8b2kWn8lhkVPXEU++BjLVCJ7yFiZMHwGZTi8UGvS4j3ZJVy1mnpwd+Nay2TFHnlQ6jpwFatQdMZgtklc1ZzkwNg4cjp5h2EtPSTCL0la5J302ZqaL2NKUTqHVW2G0aUZaK8umU88gDzGazizJVegOEi2/Wh6vzc5AEAEm2w2ajtkjQ6TUOUyuVXvzOaPQS3gGS7AjV44MJ3BYCWfH/yuoLOPh3NCbN3oPMVAtU7lPTaXqX9Vnh3mi38z09DDgfl4IXewZj4pgHHX/6BYhbS0nIQP/RW3H2XBL8vY0iv185aPGvdjPhc7+05BaSmm6y4647amPh1K5Q+2jzbkqWACAhLdWC2dO/Qb2md6FD5w5ITs6EwSBW7TfaJMJZHRBIIKEdutyOVauX44nHW6J7105uef/u4JUHasd7X34PlRsf99e5myTm97r5Pc+ltHB+X5K1MCcX9OTE1Hy/znGiWwiySiOuRwssGguKOKvmX4XfcwtIlU8veQJnoi7hyQffQrUACTPmvoGUlBRER0eLKAAfH5+seUypMe3bt0fbtm0h0qBsNvFsQl8XL150Rh8F4O991+Bp1GD1xpki6kVDJUcK8N7reEhyvF/nJQCkpWVXOul5yf1vkS6nFiVUbhwlKQDQXdau+hWzp32LcZMG4rkXHswmAjhaoRV9/GXnfny+6Es82fURvDair7NigJKOVfJjz3eomATyIwDkVAXA19dXTklJyVYRwDUFgEwAiVhB/5xvoswRABVz4pWtXjkNrSjfDY5KE5JYINvEQxntopGrPtUYpxJOtCs/Z8Y3sFjTcSnpAtq1vQPf/7AIgXX8hXig5LdRnqcoL+N2uHhZZHlpkRmY+GOhh3Ml7Fn8IvsOk8r5gZB1SbHT6Gif8t2xa3/j3/lm7bKYcPTD8cDKAkC+CfKJxU3AXQCQHAZ38xbuwxerj6BmtewLXKGauR2Umy/+tGj3221xeulqGgICvLHj637wCzBmVWRSLpHnB5gM7P3zHIZ9tB0B3p70ppGVNUACAH2JD0KXNbHSRGqX1p59QXgt1Yw1Hz+L1h1rw6KSoXc3ObzpzcTxKUtiJb1vRUbEYcqUrzFw5EhUq+oHk8n9zUdYjYjXSDBDozZkO0EJifX0VGHbtp1ISrmAj2cOFcGwjt1y9/B/5eWO8PU8cybcmlMWBICCTlnl/ZsE1pTk60Kkzf9xswBAzOmLUsRq1qwJvYdDFChMdEL+28FnMoH8E6AUKkrN+Xb5Dxg+ZBI2ffc5Hn+yM6bPmCZKior3V1mGwWAQC37K9R8yZEjW7r/jOUrCmjVrMGAAVVJqCJXqHPb9+Tc6391RvJaerQp83EIAMBhUeOONd3H69GkhTpD4oPxN0WbOXXfdhREjRqBKFSrDd+ujpAUAekb8ffcR2tOhHwAAIABJREFUjBuzWESg9nnxEQTW9YenpxEmkxmJSXHYuHEjLl9KxUfjx+Gpp+92PBPKWkAlisYWGBm/gAkoBPIjAORlAqh4ALh9Lx4BwCkiUDtVLh+IHAHAc/i2Efhs/mZ8+rEjZP++Lm0xPnQwGjXhQhS3bUD4xpWSwKQxP2PHgbPw8tQ5cutpmUr6AO2oOok4HjxlyDbHClyrUYkQfDonLd0CvTENn80ehFat/Qq++BfymAyVXYUV645i8fJ/4KnSQO0StW7V5v5Qq5IlEXVEC3+bGvhgxL14sVcb2CU71BpNARV0Kv+hxg/b9mPdhj8wYNDrqFLNCzYyBbVT2D/VuoYI/afvKtkKWaJK2I6HY41WgkpND/oeOPDXUez/41csXTJC1PLmI2cCFH11PSlFlGh1XbQrdc6VhZHrd9eflQgEWjiRUaLO4JibvPjnGVfWCAgRQKfF2DEz8fHHS7B2zdc4ceoAJoWOg06rh91GESwGpKYlOc3+XoUpU4JGq4JapRHRWuvXr0ffvi/BA3dj+ZoP8Gzvx0T0YlEPEjpPRAIUqKkzSLBZJaSkqRB7KVJESJL4QPehqCc6yPQ0KCgo29+ZU6vN1hTSNOkzhY7GDYBaAUVtac6vT05Kwx+/H8OO7/5C5MkbpRYbNKqGR5/siMeeuAc1alQvsMBaMq3lq1YUAoUVACgCQK1WZzMBLFEBwDWagD0AKsr0434wASbABApPYMrU3/D9rijxwFfdzxENINmoxKVyTUcZS7Go0qjEA196pgW08d6+VS2MGnp/oRf/dAe6joaK+VklrN16HMvXHUVyUiY89TroKa3GbYffvadWWUJCignVqhrx4ev3ovsTLSDCj7S5m8vdRMy5EybSfvR6/LHnGNZt/A2t2jyAdu06QmcEMjIzIdkMggEt9IV8IVNqkhZGoyNSKemqGXt3bYdsv4S3Rr8M/xp+hR+cSvJK2vFMupYsqsk4djIdY6eUkVUW8zl9p9dS2H/1GtWEARgv/ivJpCnH3bSYZSxbsgbTJn+Ca8nRgMoMGVbY7GZ4GGVQJZZlS7/Gq8P6Q7brRDQiCZq//xaOadOm4dKFVCz9cgYeeDhYpBwWx0ECwJEwupKShkwrBgkeHrmLCzabLERQR/SS08TVLYOpNASAWzO4kVpVHJz4GkzAlUBeAoCXl5eUkJCg5PQrf1xSXgIApQBEREQUcAMj57Ghv0oRAUD/TR+iLADwJGYCTIAJMAGkW7F112ms3hqOw2GXYdRpUbuqJzTOzBt6WNXptLBJEq6lm5GQnIl6Qb54qUdb9OvRqnBh/27YRcoNfTrZgeMn4rDwm0M4+O8lJF/PRJ3qPjDoHGZODj8Rh6lmWoYVyak2ePtr0alNbQx87g50aBfoWPw7F5AF/vTMeoa1iVSmc1EJ+P77XYiPU6NR4w5o0aoxvKtoYKWICJiF2ZVGoxf5swmxyQg7dBimjMto0aYanup+v8jLLWhIf2WbkYpzv/B8SctEWlqG8F6hQ0mdUjwRcvJGoMV/1ep+vPivbBOnnPZXWSjT290vPx/GogVf4a+//sa1lDPQqozwMOqQnnkZCz6dg5EjR+JKXAz27Y3A1i0/YOf2Y3jgoXaYMn00WgQ3dLwH3UIAKEwEzJFjgMnsJgIgdyHV9W8yp3tKLrlbjeurSiwCoJxOB252OSeQlwCQmZkpyvxRFYCrV6/mKADcqgpAsZYBVFIA6A82NDRUFRISwmUAy/nk4+YzASbABIpCwGZx5KZfS0jDjl1n8M/RS/jp93NIvm6Ct1ELvUaPDItVrNA7tK6Fh+5rhPvuqotOd9Z2rG01TrtaZyMKnIWq+HW4vD412Yz9f0fj0LEY7PnjPA7/dx0+3loY9BpkmuxIum5Fu9Y+ePL/auDeLq3x4F31AZ06++JfgZKvBrnsErntXlmtwF9/HUB42GX8eygGVavVRo3AqtBoDbDaVbCkX0dCzAV4eQN3tm+MO9o3RrNmjaBSk/GJrdh26IoyxmX5tS7GxKKZFpNVpANQnjEJAYpAoHgrUHSA0WgUXx4eHiCDb0pT4Z3/sjzK3DZ3Ako6AK0Pdmz/CT9u34eN635CuukqbDChfp0m8PVT43pKImBuiPsfaoqBQ3riwQcfhEZrgyxphZFycR7xV4ETp7K/katFvtOtj6yoHHXOaTdmq0O0rR2gRrPGxeFqVpw95msxgaIRyK8A4OIDIPn4+EgqleqmFADFBLBJkyZyVFSUHBwcXOA9jJt6424C6PygZA+Aoo07v5oJMAEmUCEI2GXKYnds+ZtNNsReTsG1xAxcTkiD1QJUr25ETX9v1A7wQZUqekcevIgmK/nux1xOQ0xcGi7HpcMm2eFp1CGgqhfq1vJBQI0bBobu3oWK+726iB+hSnSCyWRHXNxVpKVmIDb2CpKTr4uFZ2CQP3x9fVC9enX4+vpCqyW3bhcupcCo5EehdO9AzygU2k/5xvSdDmWBT1EBSsUW1wiBwux4lm6v+G5M4GYCNMdJ1CLR68rla4iPS8KVy4lITUkT85wM7WrXqYqgulTa0pF+VHJRRZKIAEhJUYyX81+N5JZjq1bBoIcogaorZsGC5xMTuN0ECiMAUHabkgLg7+8v0WLfWVLD9XvxmgCyB8Dtnip8fybABJhA2SLgWiRACAHOuoC02KbcTvq3W2UnR4Fa5yHyy1x+Lvbe5fcZ1G2hrdgYqrJqChS1Ze47Ya65pTcqjxT1Lvz6ghHgnf+C8eKzyxsBZ4Wl29Ls3Hf/89ckLoGcP058VnkkUBABICAgQOz6X7lyJZsHwK1SAIhHcewfKM9ojippDodcjgAoj7ON28wEmAATKEECigjgKgaU4O3yvnSZEQBya6p7OSk2nsp7YPkMJsAEmAATYALll0BBBYD4+Hh6OLilCWCDBg0krVYrUgCK3QNAKAouJoCyLJOF6HkAXIOt/M5BbjkTYAJMoOgEXBbbskQl7Zw728qVFTm6OGTp/La2XAgAt+oM15nO7zDzeUyACTABJsAEyhOB4hYAXFIBilcAoLW/S7iciABgAaA8TTVuKxNgAkyglAm4x/uX8u35dkyACTABJsAEmAATKGsE8iMA+Pv7y2T6d1uqACgmgG7pBJwCUNZmEreHCTABJlDWCLAAUNZGhNvDBJgAE2ACTIAJ3GYCeQkAXl5eUkJCQlYZwFq1at3kAeC0UZKUKgAuFQOKzwNAKQPo5MURALd54vDtmQATYAJlngALAGV+iLiBTIAJMAEmwASYQOkSyEsAyKQ6nw6fZJlMAPPyAHBNAaCeFEe2ZTYTQLpoSEiIKiQkhFMASneu8N2YABNgAkyACTABJsAEmAATYAJMoBwTKA4BoNSqAJAHAB2hoaFCAAgPD9e3atWKTQDL8QTkpjMBJsAEmAATYAJMgAkwASbABJhA6RDIjwBAHgCUBqCE+udWBaBEIwAUAYDLAJbO5OC7MAEmwASYABNgAkyACTABJsAEmEDFIZAfAcAlp59EgFzLAJaoAEDYSQTgCICKMwG5J0yACTABJsAEmAATYAJMgAkwASZQOgTyEgDcTQBzEgCUFABXE8A+ffrIGzZsKD4PADIBJCQqlSrLA4BTAEpnkvBdmAATYAJMgAkwASbABJgAE2ACTKD8E8hLAHA1AaT999yqALikCCjpAsUrANDinyMAyv+k4x4wASbABJgAE2ACTIAJMAEmwASYQOkTKKgAoEQABAUFyZcuXRLpABQBYDAYpLCwMCVFoPgEAGf5P7qgiABgAaD0JwnfkQkwASbABJgAE2ACTIAJMAEmwATKP4HCCgC0FPfz85OSk5OVMoHZvgcHB8sRERHFGwFAuDkFoPxPOu4BE2ACTIAJMAEmwASYABNgAkyACZQ+gcIKAO4RAJGRke5CgNynT5+SEQCc0QBcBrD05wvfkQkwASbABJgAE2ACTIAJMAEmwATKKYHCCgCuJQHdcv8VIYCi9otPAKBrUfg/RQCwAFBOZxs3mwkwASbABJgAE2ACTIAJMAEmwARuG4GiCAC5pQA4RYHiFQAUSiEhIaqQkBCOALht04ZvzASYABNgAkyACTABJsAEmAATYALljUBRBADFEJAW+zVr1pTi4uJK1gTQPQJAlmU9gPMAapc38NxeJsAEmAATYAJMgAkwASbABJgAE2ACpUkgPwKAp6ennJGRoTj7C+d/Vw8AJQWgTp06cmxsbFYKQLF7ADjD/6kSgEqlUnEEQGnOFL4XE2ACTIAJMAEmwASYABNgAkyACZRrAvkRAGiB7+XlJaenpys7/FmGf02aNJGioqJcd/7F/xV7FYCcygCuX79eHxzcItcIALtdeBHwwQSYABNgAkyACZQxAmq1GlarHTabDRqNBlqtVpT7pd/b7fZSai09t/DBBJgAE2ACTKBiErBardk6ptFoZLvdHh8eHt5v8ODB4XXr1vWJjo62Go1GseOfmZnpaup3kwBA0QDNmzeXqQqAewQA3Ug49hXlcO72043FtZxpAPSz5EgBkHIVACT+XC8Kfn4tE2ACTIAJMIESISA5P6AtFhssFotY9NMX/Z6+88EEmAATYAJMgAkUnUBCQkIOAoAt/r//Ivo988zz4XXrVvWJjr5qNRpV+RYASqUKgFMIEI3PbgLYMg8PAH6IKPq04SswASbABJgAEyheAiTo05fZbM0SAJRUP7oTiwDFy5uvxgSYABNgApWTQHx8fI4CwO7de/sNHjwy3wKAqwcARQAYDAYpLCxMbtCggXT+/HnFL6B4IwAUE0AWACrn5OVeMwEmwASYQMUiQJ/rJpMFZrM5KwKAekhCAAsAFWusuTdMgAkwASZwewhcuXIlRwHgwIF/+vXrRykAt44AcPoACBNAl11/159LtgqA0nIWAG7P5OG7MgEmwASYABMoTgKuAgAt+skHQBEAXKMBivOefC0mwASYABNgAhWdgOtnaFEEgFss+m8pABRrFQBXPwFFAMiPBwDAKQAVfYJz/5gAE2ACTKB8EiABIDPTnBUBoAgAih9A+ewVt5oJMAEmwASYwO0lQJ+vylEYAYDKAKpUqmxVACgFIC0tTUpOTs4yASSBoERTAFwwChPA8PBwfatW7AFwe6cX350JMAEmwASYQOEIuAoASgQALf7pZ44AKBxTfhUTYAJMgAkwAVcChREAnDv/SinA0k0BcO78Z1UBoM5wBABPaibABJgAE2AC5Z+AIgCYTCaR808RALz7X/7HlXvABJgAE2ACZYdAUQQA9xQAPz+/bBEA7mUAg4ODi9cEkDC6lgF0RAC0yqMKQNmBzy1hAkyACTABJsAEbhBgAYBnAxNgAkyACTCBkiXgLgBotVrZarXGHzy4v1+/fsPCAwKq+qSkXLVSyH9mJmQgU8nxV5z9pcDAQCkmJkYx/MvmAaCkAAQHB8sRERFFFwCUCAAqA6igCQ0NVYWEhDhTAFgAKNkpw1dnAkyACTABJlAyBFgAKBmufFUmwASYABNgAgqB4hAAKP3e1QPAJTKg+KsA5JQC4Pyd5DABBEcA8PxmAkyACTABJlAOCbAAUA4HjZvMBJgAE2AC5YpAUQQA9zKAeaUAlEgVAHpY4AiAcjXnuLFMgAkwASbABHIkwAIATwwmwASYABNgAiVLoDACAFUByMjIyEoBoAgAZde/SZMmUlRUlOvOv9SkSRPZ+bviSwFQygCyB0DJThC+OhNgAkyACTCB0iLAAkBpkeb7MAEmwASYQGUlUFQBwNvbW6Lyf66GgM2bN5cjIyOzeQEUuweAIgDQwClVANgEsLJOY+43E2ACTIAJVAQCLABUhFHkPjABJsAEmEBZJlBUAcC5+59NAMjJA6BEBQDFA4AFgLI81bhtTIAJMAEmwARyJ8ACAM8QJsAEmAATYAIlS6AwAoBzgZ+tCgBVCbh06RKF+4sUgLZt20oJCQmyXq+Xzp8/L5MAQD3Jcu4vQrfoGrJSBUClUnEEQBFg8kuZABNgAkyACZQVAiwAlJWR4HYwASbABJhARSVQHAKA4gGgmACWSgQACwAVdUpyv5gAE2ACTKCyEmABoLKOPPebCTABJsAESotAYQQAdxPAwMBAKSYmRjH+y5b7T+IAmQDq9Xq5VatWxRsBQLv/dLAHQGlNF74PE2ACTIAJMIGSI8ACQMmx5SszASbABJgAEyAChREAcksBUKIBKAUgLCxMiAJKFYBiLQOoRABQJ1QOJUCSZVkP4DyA2jy8TIAJMAEmwASYQPkiwAJA+Rovbi0TYAJMgAmUPwKFEQBuVQYwtxQAp2hQfBEAXAaw/E02bjETYAJMgAkwgdwIsADA84MJMAEmwASYQMkSKIwA4B4BoOz6KxUBlDKAderUkWNjY7MiAMRmfTF0R5gAugoAoaGhqpCQEImrABQDXb4EE2ACTIAJMIHbRIAFgNsEnm/LBJgAE2AClYZAcQkAQUFBogqAqxigCACKYFAsKQAU+k8lB1gAqDRzlDvKBJgAE2AClYQACwCVZKC5m0yACTABJnDbCJSkAEAL/wYNGogygCWaAqBEALAHwG2bR3xjJsAEmAATYAJFJsACQJER8gWYABNgAkyACeRKoDACgOIB4OXlJaenp2ft+iseAO4pAMUaAeDc+ZddywA6f8cpADzZmQATYAJMgAmUYwIsAJTjweOmMwEmwASYQLkgUBgBICcPgJxSAJznibKAffr0kTds2FD8HgBEWSkDyBEA5WLOcSOZABNgAkyACeRIgAUAnhhMgAkwASbABEqWQGEEgFtVAVBMAF0X/i6eAJQGULICAJsAluxk4aszASbABJgAEyhJAiwAlCRdvjYTYAJMgAkwAaAwAoB7BEBgYKAUExNDC/ysdICaNWtKcXFx2TwAisUEUEkBcBMTqDKAxBEAPKWZABNgAkyACZRfAiwAlN+x45YzASbABJhA+SBQWAHAmf+fbdHvXgbQPQXg+PHjRY8AcK8C4MQsBACOACgfk45byQSYABNgAkwgJwIsAPC8YAJMgAkwASZQsgQKIwDkZAKYlwdAcHCw3KpVq6ILADlFACgeACwAlOxk4aszASbABJgAEyhJAiwAlCRdvjYTYAJMgAkwgaKlANyqCoCy81+nTh05NjZWmACSABAREVH8AgA9LChlAFkA4CnNBJgAE2ACTKD8EmABoPyOHbecCTABJsAEygeBokQAuIT4i0W+uwmgqwDgrARQdAHAPQWAHhZUKhWnAJSP+catZAJMgAkwASZwSwIsAPDkYAJMgAkwASZQsgSKIgDkFQHgVg2ASgEWXQBQUgBICFDQcARAyU4SvjoTYAJMgAkwgdIgwAJAaVDmezABJsAEmEBlJlAYAcC9CoBLqT9RBaB58+aywWCQwsLCRFRAkyZN5KioqJIpA8gRAJV5+nLfmQATYAJMoCIRYAGgIo0m94UJMAEmwATKIoHiFAD8/Pyk5ORk13QAJS2Adv9LRgAgqGwCWBanFreJCTABJsAEmEDBCLAAUDBefDYTYAJMgAkwgYISKKwAkFMZwNwEAGfUQNFTAHIqA8gCQEGHnc9nAkyACTABJlD2CLAAUPbGhFvEBJgAE2ACFYtAYQQApQxgfk0AlRSAYvUAIC8AZ/h/VgSALMt6AOcB1K5Yw8S9YQJMgAkwASZQ8QmwAFDxx5h7yASYABNgAreXQGEEgLw8ANzM/0quDKC7AMBlAG/vZOK7MwEmwASYABMoCgEWAIpCj1/LBJgAE2ACTCBvAgUVAPz9vaSEhATK51e+hPGfsuhv0qSJ5DT8c/cCKN4qAM5qAEoPuQxg3mPNZzABJsAEmAATKNMEWAAo08PDjWMCTIAJMIEKQKCgAgCQqSzscxQA3CoCyA0aNJDOnz8vzi3xFACOAKgAM5K7wASYABNgApWWAAsAlXboueNMgAkwASZQSgSKSwAICgqSL126lC0awDUVIDg4WI6IiCi6CaBz518mM0BipFI5vpHywB4ApTRr+DZMgAkwASbABEqAAAsAJQCVL8kEmAATYAJMwIVAYQUA9yoA7gJAzZo1JY1GI8fGxkpkAqjX64tHAHCtAqB4ACgCAEcA8NxmAkyACTABJlB+CbAAUH7HjlvOBJgAE2AC5YPA5cuXRUN1Op3SYPr4jd+3b2+/fv2GhQcEVPVJSblqValUcmYm5f07UgByKgPoDP/Pyv2vU6dOyQoALog5AqB8zDduJRNgAkyACTCBWxJgAYAnBxNgAkyACTCBkiVAEQD0eUsHRdNrtVrZarXGHzy4/5YCgFIG0CkCiLB/1wiA5s2by5GRkSVrAuiaAhASEqIKCQmROAKgZCcLX50JMAEmwASYQEkSYAGgJOnytZkAE2ACTIAJANeuXYMkSUIEoC8K27fZrPEHDtxaAFAqACgCQGBgoEQRAq4eAG3btpXCwsJIWSiZFABFAKBBDA0NFQIAewDwlGYCTIAJMAEmUH4JsABQfseOW84EmAATYALlg4DFYoHNZoPdbhdCAOXqe3gY4v/8c3+/3r17hHt63kgBoIV/ZmbeVQBuFQEgogyKikXxAHB+F6qFIgBwBEBR6fLrmQATYAJMgAncPgIsANw+9nxnJsAEmAATqBwEjEZDVkclidIAyF9fjt+zZ0+/Rx55JLxu3bo+0dHRVqPRKEr5uQoArikA9H9+fn5ScnLyTR4ASsRAsQgArlUAnBUAlOtyCkDlmLPcSybABJgAE6igBFgAqKADy91iAkyACTCBMkPAVQCgRonlfx4CgOIB4FLmTyz63QUA5f9LrAqAs8FkXiBMADkCoMzMK24IE2ACTIAJMIECE2ABoMDI+AVMgAkwASbABApEoDACgFIFgL6np6cLE0CXxb4UFRUlcv+VKgBKBEBwcHDRUwDcIwCcpQBZACjQsPPJTIAJMAEmwATKHgEWAMremHCLmAATYAJMoGIRKKwA4BLWf1MVAJfIACEElFgKgDIUSgQAmwBWrMnJvWECTIAJMIHKRYAFgPyONz1bOQ9ZC0ADwJ79xSqXc/J7WfHMRoeackKzvUqWi2bj5NywyVZ6igI4yYBKo9HAarWKclT0pVarxb3p/+jfolyV7PidSuX47jhutFF29t8lPTTfvc7PiWqNzdEeaKBSaUB5s472KFwcv6BzqPlK35Q+yZJO9Iv6SbW36TzXQ2l/ftpSuHPymg+uXAt3h9xfZQOUOSnGUusYU/E7+irp+7v9eWSNm3MmOUuilUTP+ZpMoKwRKIwAkFMKAJUBTEtLy+YB4CYEyH369Cm+CAB6z1U+TJxRAVwFoKzNLm4PE2ACTIAJMIECEGABIHdYjkWjY8Gs02lAi3Jyc7ZYbDDojc4VaV4LvVvcQ2XJWmTTokwFXfYFahEFAGq7VqsVi2Bl0UyLYWcJKnEv6he5UtPv6Vxyqc5aYMvO1zkXjPT/jgU4iR8Qr1MW3QWYcvk+lQSRGyKGQ6hQSmiRKOB6aLQq0XbHwhaiLxqNTpxvyrSQ4/ZtEQAMhhvGX2az2a3vJb0At0Gnd3KSSQhxijr5FACIP01/xbWcGk9zieYMCS4FFajchSKlJnq+JwSfyATKMYHCCAAuO/rKDn9WCgC92SlVANxTAIpFAHCtAuB441chJCRElAFkD4ByPBO56UyACTABJlDpCbAAcOspQAtKD4MndDoVrifbcOVKHPR6DWrWqg5PLx0yM90XdPmfTno9LVBlyBItugG7nAaIyIIbBy2waOdaq3UsFEl0UBbwrov6W92VFmoXLlzAjm37kJiYCKN3PYwZ21MslGNjY7Fz+0HEx8ejZs2aeOGlJ+Hp6SkWe8qzniy7RDjIWmg1OmzZvBOH/j4Og94Tffrfh1atWglBpCSOGwIG8Ofeo9i7+4hY2Hd58E50uKsltBo9aBOZ2qzR3NjZJkZbNu5G2JHz8PHxwnvjBmRFODg65xAJSn4BKuGXn/7Bf8fOiPt1e+ZetL2zKW4IASUrAFw4fwnfbdkLk8mCZs3ro0evhwoUAeBY5JMIoxJzUBGAFAFM+Xd+x54FgPyS4vMqIoHiEAACAwMllUolX7p0KZsfgKsJoNMXoOgRAIoA4FZSUHgAcApARZyi3CcmwASYABOoLARYALj1SNOu8Zmoi5g2ZS7Cjp6B2SRBrZHg66dFt+6PYvSYEW4vzv+CbubkFfjtj5WQrL7o0aMHxrz/KqzW7IKCTmfAxYsXMWHCBJw6dQqBgYGYPn06GjduLIQAJWz/Vj0g8SDiWAye7/s6Tp8+jbZ3euLvv/8Wi7qTJ09i0CvTEBYWhs6dO2Prd3Ph5eUlxAFa2Cnh8o60BMeC2ejhjTHvTsIn85fAoPbFpu8X4qmnniySEJLb35ljgS7B01OPD99bhAVz14ufZ8x7A68M7iaEE4oEyIoKUNth8FBjxpSvMCXkS3jqq+LjRW9j4KvdkJnh5JWVpiEVeAe74O8JEkaP+hTLv9ohXrp28xR073F/qQkAP23/EwOeD4FNtuGRRzpj208fw2qxFygFgOaBh4ceB/cfx9SJX6N128YY+9FLqOJHc6VgkS8sABR8BvErKg6B4hAA6A3RPQWgbdu2UlhYWOl4AISGhooIABYAKs7E5J4UjICyc6AqusZWsBvz2eWLgMqxg1JSObLlCwa3tiwSYAEg51GhcP9Df5/Aa0PGI/xkJNQwQoYWEjLhoVbBJkm4q31nrN40BbVqVxOLUUcavVMEyMq1vvn6tPv/1bLvMfKNsbDbVej61AP4YsVEVPMOyNqVpgW+t7cPvt+6V7QhISkRbVq2wdYfP0ZAzaowmxz3c+S7Ayr1jdx85f2GFvK00O/T+0MhANzRzgsHDx4UwkF4eLgQAI4eO4oHujyAzVtnw8fHJ2s3X/gEqF1SElQSDHoPTPhoJubO/gIGnRfWbVqMp56+VwgArp4BWT2mtAa3vG9XGu45+e6kKILBZrPA29uACR8uxtyZq+DlZcCc+W9hwKCnYMq0Q6vxyIpa0Oprfly8AAAgAElEQVSApKQkPPf0WPx9KBwLPv0Aw0f1htlsz5Zq4RA0JNjtxIzGS9lMU8aPtr4pzJ3A5rLIdXokZG+3i2+DypqrAJBXCH2hPjdc2rT7t4N46fmJMJnM6PHsg1i5fko2AcDNEuGmiapS2UUKgM2qwrLFazF+/ES0ad0eW7atRI2avjd7YNwYeMdPquweGe79sdutub4lqtXZI2LK4vsnt4kJ5JdAUQQALy+vbFUAqAygWq2WEhMTXRf+JWMCqHgAUEdZAMjvcPN5FZEAhe9du3ZNPHSo3XI2K2J/uU+FJ2CTzCL3tEaNGlnhk4W/Gr+SCRQ/ARYAcmZqNlkxJXQBFi7YhCpeVfB4107o0/dRJFxNxcKPNyDy1CXIsGBS6Kt4Z+yLzgUmLSbzJwCcOhmN14ZNxx9/HUG92nXx2Rfv4onHOmUtZoWxnUqPD8fOxdJFmyFDhVkfj8CQYS9Ao9ZDq6PQbFqgOczcaBxpx592ZW1WWtza4WHUIyIiAv37ThJCQHBrPY4dOyZ2oDMyMhAXFycW/CQU1K9fX+R2u5oAenl6wmJxLIANBlIbgLFjJmP+vNUw6DyxbtOiLAGArmMwZPcwoAUfXZvaQmHoJDxkT12wiTbTQYIHHdn+LevFApysB86fi0Vqaqpoo19VX9SoUV14EWg1BnFtDw8dSACIvngZJ0+cgX+AD4JbNQHl4JtNNwwNHQt6Gwwe1C7JmXbhGrlxw5RR7JaLhWzOIoAwGLQ4fBAMHmRU6PAhoO96vRqZmbkLABRdkVv/ZTm7z4H7TM26v1oW7Ol6NP7K/b/fug+D+k9BUkYKej39MNZsnp5NAKAFCbVX2cnXaBzihZhDNptgTde1WdUYPng6vl2zCh3b34Effl4Gv6qewlSQxtZuk4UARWkC1CZa2NM1aAFPv6M5RWOk9FXphyxbszwKFNaKZ4Hj3ywAFP87Pl/xdhEoLgGAIgCUFADFA8DdBFC8bRW1o+4eAOKiDhmPPQCKCpdfX+4I0AfZ+fPn8c8//zhCMOkBhQ8mcAsCZlu62FV75JFHULVqVebEBMocARYAbh4S2v0/diQKz3Z7G1euRqFFcBBWr1uEpk2bw2BQYdGC9fjgnaWwyhno3v0BfLshJLsAoOzC5lEVYPTouVi6ZCt53GPWJyPw1hsvZy2EaSEVG3MVI16djJ93HUTdGnXx9epxePjRzrBaVDh9OgoXLkQjJjoBaWkmVKnigzvbN0f9BnUcofxWCXqDFidOnBACAAkBrdt64NixI0hPzxQL/ytXrjgd9NWoW7euWLzRfPDyMiIhIRF7f9+H6OgYeHt7ou0drdChY2uETvwEM6Z+BYPOiHWbluKpp+8WEQC04E1JScHx8LM4GXEOKSlp8PL0Q8NGdXBHu6YIqFkNaWkZTlNCytuX4WFUI+rUJUSdvoSoU9GiLY2aBKJRY8eXSkXO/bQQVSE2Nl6IFrTYJAGgdu0aYmFPAisZAMbHJeHC+RiciryA9PRUePt4oGZNf9SrXxsNG9UX40Of2R5Grej31fgkWKwmBAUFwb96TZw8EYVjx8LF/7VqFYxWrZsjIKC6I83iFuNI6QfCp0AHpKam4/A/ETh2JBJ+Vb1wZ/tmaN2mRa4RAHqDGhcvXEF42FlcOHdFsA+qVwMtgxuiXv1aov+5HRYzpTw4FtnHw6Pw94HjSEtLQ+MmdXDfA23w39Fo9HlmHK6lJ+P5no/h2w1TswQAmuOXoq/gRMQ5nDsTKxb8NQKqoVWbBmjcJEiIR1aLjKSkazh54gLeHLYYUWfOoHnz+pi38C0E1PJBrZqB8PQ0gvpBmE6fuogzUdEibYb8KuoG1UNgXX+0aNkAflW9b0oZ0GjtOB0ZjbNnYnH+7GUxJwODAtC0eZBINbDbchdAytybKTeICeRCoDgEAPIAiImJkSkCIDk5Wez416xZU4qLi8uWAlCsJoCKmEBvUEoEAJsA8lyvbATow54eqCiM0iEAeFQ2BNzfAhCwSunCWKtbt24iCoAPJlDWCLAAkLMAcDryEhYtWCUWVLSIfPf9l6DXe8DT0wNrV/2KkUPmId2ciO7d78e3G0Idj0jCxI/Cx507yrkIAJQGsGLFTowcOh0Weya6PNAKK1ZOQUBAgGgQ7ZZu2bQTg/vNhNmegeeffxSLlk6Al5cn1qz6Hp/MXocrcdFISkwTAezeRl+xGH72uUcwc94oUbrP6KkXn1cvPj8ekZGRQgA4fPiw2LWl348aOV9EBNzTqQlWrf9cCAceHgYcPx6BoUNm4fSpC1n56gaDBqPfG4zz52Lw5RdbYNADGzd+7hQAMsViMnT8Uuz69R/BzGy1wKCpJkL2SZT45PO3cM99bbKJAKtX/oAZk1cgM8OC5OQ0WGwW+Pl6i74PGd4dH04YDJPJBC9vPULHf4lln28R7fsoZDBeGdJNcKZz9/3xH8a8+SkunKdFpEm8hnbDfat4i0oNo8e+gjff7YPUlEx4eumx69f9GPrKNOj0Ej6a+DauXr2KT+f/D2kptJutFmNMi+Fv189A02Z1HWkMOYyl3QboDRrEx13DqGGzsXcPsbWCygsaDHo893w3JCWm4Lutf4oxdfUAoMiEhfNX4fMFm3A9OU2IOPR8Qff28TVi+Khn8eboAbm+XciSI/JgxuTlWDR/vfCQMJszodOrcUe7Jrjn3ruwad1uXLgUi25PdcH6bbOEAEC77GQOOPHDz3A5NkEIOBJkGA16IfYMfu1pjH7/Bfj6+mL5N2sw/NUP4WvogLSMBGhUnjB4peL+B9pjxuxxaNmqrli4T530JVZ8tR1Wmwnp6Rli3nh6VBf+AfT3Q+NPoojr8fHsr/H5gi3OiBSL8Cqo4u0Fo6cBz/d7BJOmjBRRCHwwgYpAoCgCgMsOv/AAKBUTQOfCn5QFEU3gKgCwB0BFmJLch4IQoGhLIQAccEQAGNTOMlAFuQifW2kIWGUTvL298dhjj8A/oHql6Td3tPwQqMwCQF4u8BS+7jCZU2rNO8riDR7wIVat/w1GtRFjPxqACZMHiRBn90OVY4644yy6Nu1aP/HQUJyNugyt3oztvyxFp04dIcMGSbJg+oyvMGPqN9DDgPGThuKDCa/g15378dqrIbgYG41qxgZ4tGuw2K39dUcEriRdgwQTFix6G8MHPweVzkPk+lMEgCIAHD36r9idpc+x11/9GAcOheP+Lm2xcfMM+FevjqsJiXiu1zjs+/MYdKLUngpNmgQh7koiMjPToVGT6K2FTY7H+s2fiRSAq1eT8O6ohVi9fg089d64s11LNG1eD7HRKdizOwwy1GjdujbWbZmDunUDRVWFtau3Y8jQ8bBb1VDbDbjn3jao4mvEvj/DkJqWDiskfP75uxg6vI/gNXHcZ5g6cxWqe3phzidvYsCr3aBVq0Bh7i+9+C6owp4aetSuFYSWrWsh7koywsOjYIEdEiz4dP6bGDpsgIgA2PHDbgzsF4LEVAv0Wq1YtPoavIQ4kHw9HZl2eo2M/n264n8rxokIhJwM79QqLSTZhpf7v4ktW/6CHr5ivtSrH4jEhHRcT4+HlqIY6PWSHZu2Tkb3p/8PMmQsnL8Gb78zC3oYUcW7Gu65r7WIHNj929+4cjUaksqOdes+Re/nnkBGJgkaNwf0ehgNWDB/Ld55Zy58PLxgNVlRo1pVMR+iY+LFfYweHkg3ZeCZHl2wZtMMaDVa/PzTHsGMBI/q1Wug870t4O0L/PFbNGIuJwG4hiVfjsfgwS9i6ZKvMHLEB6hiaCcW6nqdF0zWK3js8S6YPedNtGhbDy+/EoI1K3+BWlbDoNGjafO68A+oilMR5xATf02MpV+1dOzfuxWNm9QTURVLP9uI19+ZK/jUr18Dd9/dQggg+w8cxcWLl6FSq9H76afx9eqJt3wzLZRHQvl5a+aWVjACRREAXD0AchIA3MsAEroipwC4CgDKhyVHAFSwWcndyTcBIQBEnMLBgw4BQK+ivLvi+DPLdxP4xHJEIJsAUKN68bwjl6P+c1PLPoHKLADkNTqKCR4JAP/+exR79+5F2OE4bNi8Vwh7QwZ3w7sf9EfVaj5Zufuu18xNAKBFEO2qvzliFpYt2QS1yo5hI17EzLljodFKoBJu/ft9iLDDUfAyemLHbwvR6e5WWDR/Az5b9C1kjQX9X+iD9z4cKBa161b/jJdfHger3YRhw3tj/uwPoTUaEP6fwwMg8tQJEQGQJQBEROL1oXOzCQDVq1XDju37MOiVqbialIjWTRpiwuRhIizcbLZi1fIf8dnCjdBrDLDYE7D5O0oBuBfnzsZiUL9puBgdheBWTTH7k3fEdyox2L/PR6KEn05vxe/7v8Wd7VoLU7p7OryAYyeioIYHQkNexxtvPQ9ZsmD79t/x4ZgFiI6PwYP3dMEXKyaJdICcBABLJjDitdFYt2ovVKiGfi89gNHv94O/vz8Sr6Vi6+ZdmD9/LZKTk/HQ/7XFwiXj0bRZA+z44XcM7DcJqSYrrFYb+r/4OEa+0Ut8lh88EIGZM1cg/up1tGneFGu3ThFpDDkJAPRA//uuI+j9zJsiioHuO2/hO+IeNquM9at/wLIl3yHTSktgGWs3TMCzvR5FSmoGHug0BCciz6Np4waYPG0knuh6r4h0+HH7H3jr9Vk4d/ECOtzZEgf/3QCzxSpSIbL8JZRJppYxaMAUbNr4K/Q6Dd4Y9QKe7fUwPL08EHHyHCZ9uBTnz1+GBClLAJDswLcrtmLurK+ghgGj3nkBA4c8Aw+DDqtW7sQrL78DDbww9LVemDVvLBISEhB25CTGvDkfMTGX0bJlc0ya8gZq1fFFqxb1cfrcBfR45n1ciI5Fo8BATJryqoj08PQ24nJ0PGZPX4fvf/gTZikREz94HSEzhiH6fBzeGjkX23f+hbp1a+Kr5ePwwAPtRK++37IXMyZ/jStx18S8W791fl5/pvz/TKBcECiMAODp6SlnZGTQJrwS4i/C/l1TAJwupsWfAsACQLmYV9zIEiXgMFmigz48T5yIckYA2OGhdRj/8MEEciJAobsUVvv444/CnwSA4pJlGTcTKCYClVkAyCsCgAQACpencPqVK1dhxIgR0MqBsMMfTz/VEbMXjBHhzXROTkduAgCdTw+Ea7/9DYMGjBU17es1qIFdf36DGgG++GnHPjzbczRsdjueeer/sGLtVGFol5R0HRZbJgxealTx9INs90JGugknTh7H44++jkxzGl4a0A2fL5gADx8jwv+LRP++Ex0CQBtPHD12yBEBEHEKrw+dc5MAMHPaCoSGfAmrZMPMGa/ivQ+GZlUmCDsWiX7PfYAzZy5ArbZh41ZHFYCkpFSkp1HouQyj0QiDwSjyx83W63jjtTnYtHEnDAYbdu75Cnff3RHHjpxFz24jcOFyLO65+y788NOn8Pb0ER+w6aYUjHtvIQ4dikBQnUBMm/0mWrSsn6MAcPL4Gdzf8VXYrDpIshlHTqxGkyYNYLOTJ4EWyckp6PfiJPz2y354GXTY+P1MPPLoA1kCQGJaGtq3b4Et2+agdi1/qDUa8Zqnu43GgQP/oXFQY2zZMRNNmgXeUgD47LOFeGvUbBg19dDtmS5Ys3mK8CYw6PVIvJaAgf2n4udf/gbUKqxZPxG9ej+CP/aGoW+vDxB7LQGPP3Q3Vq6ZemO/TmXHgL7jsXPXATQJqoefdn+OBg1rO6szZC8zmZScjD69PsDBA8dFlMbGzdPRunVTUGoANGosW7wRo0bOAVUrUiIAyNySBBGTOQXVqvmLEH0yetRpjTh65Dge6vI8IPvipZe7Ysac0QgIqIbMTBs6tHlWeAB07tgR3/34Bar56wG7hPXrd2Lk63NxLek6Xh/eC9NmDhd+FKnpGfDxMuL33WEYNDAU5y7G4OlHu2D52sliPr05fDbWbNmJGtX9MGRIdzzX5zEE1KgGD50eap3Dr8EuS6hSpcot3+n42auYPgT4MqVCoDACgHPhL7tHAKSlpWV5AOQkABTXoyatbkQKQFbpMzYBLJXJwjcpKwRyEgAOi50DtVDlHaWY+GAC7gQoAoBMAJ988nEWAHh6lEkClVkAyOt9m3KqyWme8sj//HMflizchmPhVxAZdRLeRk+0a9sSM+aNQLsO2XOblYHOSwCQYUZmhh29uo/CnwfC4aHywMcL38drI3vh1VemYuXqTSBtYd68t/HW6P5IS7OInPoMUzr+OXwcF6Mug6oJkFng+bNXcOjfs0gzJWPAy0/g03nvw8vP2ykAhCDy1HEXAcCCExFRNwkAtAgf8/YCLPvfNmh0aqxaMw69endDRroFkiRDq1Nh7OgZWLJ4PQw6A9Zt+jSrCgA93J49cwlH/j2BkyfOIvpiLJITzfj7QASuXE6AWpeG3/74Ep07t8eWjX9h6CuTkJgRhz7PP4nV62YiPcUMnUYLm2TFtWvXAZUGdptJeKf4+HjlKAAc+zcC99z1LAXdo0Xzutixey6qVasmBBmHOZ+EQa9Mx/p1O+Gp1WDTDzPx+BMPZQkAV1MT8czTD2PjplmwmBxu/la7BT2feRd79v6DFvVbYPP2WbcUAAx6A8Z/NB0zZ30BT3UtLFj8Ll565SlYzIBGQ+Z8FowcOg/frNwBtVqTJQBs3bwXQ1+ZguS0NATVCkCbNk2yvTf8918ULl1JQDUfD2zYNgf/91AnZGZm3hQBcPRoBEYOn4nw8LPo8/zDmDXrbVT18wNkFYw+HtixfTf6PjsRFrvVIQBsnCXYkGmhRg3s2XVMpIKQed+V2FQkxKfiwF8RMNuS8PLAZzDnk7Hw9FIjNcWKh+4biNOnzuGOti2xYesi1A70hmSWMWv2ckyd9rUI61+y7D282PcJR4Uk2hyR7LiekoaePd/EwQPn0PmOVli+JhTNWtbDF4u3YOioGYAkQ6/Rw7+qN6pXr4q7770DXR7uIAwtW7dsCLP55tQaBVZeAl6ZfMPlRlVaAkURAPLyACjxFABl1EJCQlQhISFcBaDSTuPK23H6wKHSSVRSSdRflnJ36a28pLjnDmXILh6MWrRoBn/FBJC1Ip4cZYhAZRYA8hoGcrantC+q1075z+SU7uHhgflz1mDK9GWQzGY8/WwXLF89BZKkFvnfN+rK06ZvHn/sshpGow6vDZ6Cr77ZLHLrX+r/OD7/30S0bdYbF2Jj0bhBXazfMhMtgxuLnfvkpDSM/3Ax1qz+BXa7CTq1s0SdbIBKZ4DZloqXBz6FhZ+Mhd6TIgBOOyIAIp0pACICgASAs3h96KxsEQC0eBzx6mx8vfwHaLRqrF3/EXr06IbMTFqEqWD01GHMO9Pw2YLVQhjZtI1MAO8V11u1fCfmzlyBU2fPkWuCCElXwxtmqxUqSLDhOvb9/Q3uuqsdVny5G2+NnIFkSyxeeKE71qydgbRUC3RaWrtS2T9aPGshyzZkpNtFVYKcUgCOHf0H93R8ARo5CE907YzFX74vdrWpUgAJAOQy/9qwmVj17Y+oYjRg8/bZePCh+1wEgGT07OEqAGhuCAB7/kGz+k1yFwAMBowdE4J5n6yEl7o2Fi59D/1fdiyA6flArZLxzqhPsfSLbVkCQM9ej2Drpr0Y2G8iZBUxkqCSpRsmg7JWeCZIKhV8vW1YsWYOHny4szD4c08BOHz4PwwbMh0nTp3FywO6Y8qUkagZ4C/mrMFTj++37cIrL05BhjnTKQDMgSRbhJHj9NDl2LJhD9KtydCp1NBp9aLqgCypYbal4ZVBT2Pu/PfhYVQhNcWCh+4dhFOnzqJD+9bY9N3nCKjlCXOaCVOnfoU5876ldTw2bJyKnr3+DxazDRabFV4GLySlJKBnj1HYt+8Ughs0w5Ydc4UAkJ6WhtDQlfhiyWZYzWaYrZlwJDnoYIcGvlV8sXrVm+jatWuWEWVef6/8/0ygLBMojABAKQAqlUpOT0/PSgFQPACqVasm1ahRQ46MjBRpAS6RAPRz8XoAsABQlqcWt+22EBB/ZnwwgQISYBGggMD49JIiwAJAzmQpf33fvt+RkWFCy5at0LFjRxG6TK7kZIZ3d8ceOHP6OoJbNcaqDdPRsFFd4UpPYgGEL39+HsAkEYJ99Egknu32Bq5cSUe7ds3Qf0APzJjyFeKT4tCv35NYvioEqalmYZ439u1FWLx0k0gZ6PrkXejavbMonXc1PhmDB02G1Z6B/i91x6fzPoRnFa8CCQC+Pj54Y/jHWPbNVqihxvr1k9DruaeQkZEpjPBIBPno/U+xeOl6GNTISgH4c+8xvPjsBFxLjEOjRnXRd0BXtG7TFHUCq2POjG/ww3d7odaYsOvP5eh4Vzt8t/kPDB30EZIy49Gr15NYs24uMlJtIgJAUtkQG3MFSdevQ6NSoV79IPj7++UsABw+i3s6PQ0N/NGiWQvs2DMD1ar5wm5XZUUAvPzSFGzeuAveBi3WbZ2GJ558OFsEQM8ejzojAGRHBIDNhp49RmPPnn/RrH6DPAQAHSZPnoypof+Dh6oRJk59FW+O7pMlANAu+1sj5+Orr3eIFIAVqz7ECy8+iR+++wuv9J2A9AwJT3bthAmhA52T0BFtKJ7kKYrf5ovagVVQs7ZvVnlI19l68uRZDHp5Co6Fn0S/vo9j5qw3UKN6NdhsgJevET987x4BMAcmcxreGTUH336zU6QGPNm9E7r3eADNmjXC5SuxeKnvcEAKwsuvPCHSL/yqGrMEgNOnz+HOO1pi47bPUDvQB7ZMKz7+ZDUmT/kSJqsZ364MQb+XnhDpIPRnYNT64nLcZfTp8zYO/n0S7Zq3wupN00WZQo1WhlplQGLidfy5+xDOnL2Eo0dPYfeuo0i8bkK62YQGDa5g//79uaYBlNT7Il+XCRQ3gcIIAEoKQEE9APL3+ZN3D29KAWATwLyh8RmVhAALAJVkoEugmywClABUvmRBCbAAcDMxyrXftWuXKN9Ju8kvvtgXX331tagGYDBAuN7fc1cPnL94HW1aNcM3qyeLBW9SYrrwC9BoHHXRdVoqC5jLIZOHDGCzWzDopQ/x3bY/UcW7iqhzHxuTALVBhS+/mSDyxmlXOeZSHF4fNgO7fjuCeg3r4Psdc9C8RSNxg592/Ikez7wFCWYMHNxDeACodFqnABCCyMjjaN3WiKPH/nFEABw/L67lXgVgzsyVGP/RMlhlG6ZNfhWjxw4QZeNoR53q3PfrPQmxsdegUqdmCQCfzFmLkHFfwCalYsHn7+PV154DbWpnmK7j+R7j8OuvB6DVWPFv+BY0a94YkSei0eOp13Dqwlnce3cnbP/pM/j5+gjjvExzhohM+HHHX2jUqBaWfTMebe9omqMAcCoiFp3bDgNUmSLa6uDRb9G8RUNAbRLyS8LVZAzoPxm//LYfNav4YMN3c9ClSyfs2L4XA/uG4Goa7U67CABQw2q3ouczY7Dn90P5EAA0WLRwGcaOngutXBf3dmmNrT/OgsFDB41ag5joeAweMBW//xGWzQTw9z1HMeD58Yi7moSHH+6IbTs+FiUcqXShXbLg80Vrsef3w6gZoMWwoSPRum3jHAWA5Oup6NtnPP766ygCA6vj29WhuO/+9rBZ7JA1dmEYOW7MEthhz4oASE27hh5d38KBAxFo0ayxSDFo3qKBmEOUMvBM95egQUMMGvI4Zn/8XrYIgJOnotCpwx3Y8sMS4QGgVamxZctuvDZ8BhKupaDfC49h4eIxqFrVR/RXLevw4469GDliFi7FxKP7413w9apQISR9+812JCSk4c52TdGz9wNijqVnmHAx+goGD5yBv//5D7Wr+mLFmhl4+LFOHAVQ0Dd2Pr/MESiMAHArE0DSCCkCQK1WywkJCaUXAeAUtjkFoMxNL25Q6RNwhl+W/o35juWFAK0acgoFzqVEWFbXWCQoL6NcbtvJAkDOAsDhw4fR59nZuBATjTYt7sCkyT3QvHlzUaf9y6U/YM68FZDlVDz4YEes3TwTnp7ewtDvyyU/CMf28ZMH46GH2+c5L4g/VQPYuP5nDOw3HmoqJae2QJb0qN+0NvbsWyKqAOgNapGD/daIedj7xxHUrR+Izxe/j6bNg0RaQOj4L/HjTwdgwXV073Y/Zk97B01bNSyYAOBfDb/9cggv9QvBtcRkNKpbG+MmDRJu7CRArPj6Byxb+hOMGi+Y7LHY8t0SkQIwZeJXmDV1JaCy4q0xL2L4631E6P6BAwfx1vCFMFtsUMGCZd9MxEOPdELNWv54+L7B2P9PJLQqD4wZOwADBz8FWTLh4N/HMP79zxBz+Soe+r+7seSrj4QJXk4pAHarDW+MDMXyr7ZAp66Ox57sjElThsPgoYHVaseWjbvx8by1SM9MR+9n78fsj99H/fq13AQASgGYA4tJhkoIADb0fOY97PmdUgDq5xEBoMFvP0cIQ0MVtNDpNJgwZRAefKQ9yGxv64bd+HTBBqi05G1gx/pNk9Cj58PIyLSg8x0v4/jp42gU1ABjxg5Cz96PikU+VW0Y9+EchIWfQP0GwC8//yX6b7VRCkD2w2qTMOjlUGzduhtalQZPd78Xb779Irx9PHDy1AWMefNTXE9OF4aOwgNgwxykpl/F00+8gcP/nEL9eo2waOk4BLcJQlzcZUz44H/4Zed+kYry4IPtMX3uCLRp2ywrAiDy1BnUDQzA8tWz0bBxDdTyr4HTZ6PxzDNjRCnLgGpVMHPu6+hwVwshAKQmpWLhgtXYtGk/7MhEyPgRmDBlKE6EX8AzT7yDc5cvolHj+pg3/220Cm4MLy9PXI6Lx6gRM7Fv379oUKcKtv24GE2bNs3z74hPYAJlnUBhBICcIgBcywD6+/vLderUkcLCwjgFoKxPAG4fE2ACTIAJMIGyQoAFgJxHgnYpx4+bj/8t/g60fKVFb7Pm9ZGWlobLsVdhsplRvZoFX66YjocfflhcZN7spZgQMgcq1WWsXLkS/fr1FSXvcjuIPz0YHj4UhldeDMXps5dhUBtglq5h0hli00QAACAASURBVKR38PZ7L0CrVYsFOEUKvDHsE3y7dgcM0KG6vzfadWiB8LBzuHI5CWqVxpmCAFF95M9/vobNnolePUbh1NmTuONOPxw5chAWswYR4RcwctgEHPr3BO67505s/H4OvL18xU7rC8++h12798FDVwUWqx1GgxfM5nRYkIkaVeogM9MMky0aW7cvxmOPP4jvt/2K4UMm41qSHT5GPe5o1wSyLOGP/WHQQActtGJ32ybbMGRodyxaNhbfb/0DL7zwEUwWyvxWof2dwdDq1Ag7ehpmMl+EjC+/mIB+Lz8q/AQ+en8xZs9eCV+jL+YseAP9X+4KtVqLw4fC8Uq/MbhwPhVUiyGwpj+CggJxJuo8kq+nCF8GWtgv+t976PvSY/Dw0GH79/swsG8oEtKvoWePB7Fx8xxYMp0pAEIAGIM9eygCoBHWb5uGNnc0QWpqukj/cD0oZUCvM2DggDFYs3YnvPXVYLZYYPTwEFEWFsmKKl7eIo2Exnn9tul4omsXIRSs/GYbBr46HJK9JjTwxMOPtIPeAPyx+zjMJsAmX8erA/th4bLRt9z99jAa8e3yHzFsyEzoVDrYZAs89I6oE5MlE95Gf2h0ZlxLuYpnuj+Ajd/NxfVkM3p1fwe//7UfHqiCBo1qo2WrBjiwL1wISa5HvQY1sen7OWjcpAE6tRmMk6dPQ69VoWYtP3Ttfh/eere/COd/feg8LPliC7xEeUgbagVUR2BQAE5HxuB6+nUAZlSpnojDh/9AnTp1RH9mz/gc06ZvFD4ZNQKqosNdzaHT6XD031OIvhgHm2xFp7atsefA0rLyVsntYAJFIlAUAcC9CsClS5do91EiAaDUIgDoTUzFVQCKNAn4xUyACTABJsAEygIBFgBuPQrJ169jWsjX2LzhV1htlqyFGC36atfxx4fjB+HZ5x7LusBnny7H7GmrYJbOYsWqz0UKAe3q5nWQUSj5CkwNWYwvl22Bl7EadAYJ366bI2qq0zVo0UQmfD9tP4AZk7/ByYgLkGEVpWmp0siEKUOEODF5wmLIKhMk2YSwiN/E4nPooFAcPXoMne4JxO9//CBy5On1bwyfgaP/nkbne1pjxdop8Pb2FmZ0x8NP4YN3FyA87AxMmbSsVsPDqMY7Y/vC6OGLJYs2ICHxItZsmI+HH70H8XFJmDJxGTau3wm73QabPQN2u4x77r0Trw7vjbkzVuNM1DmkZyRjwOAnsOyLKcjM0GPjlp8xLeQrJCYmw2QywWyyCJNFKv02dERPvPfeALGQ9vU1YvLEL7Dokw3wreKN0Omv4bkXaMfcDp3OE/8cOoGQiXMRFnZW5J9LKjIAlOGnqy7OHz6qN15/63khXHh76/HTjv0ikuJ87CX0ef4xLF8ZAqvJUc2HzOv6952En3/eg5aNm2L1xulo3rKe4EjGhO4CAP07KTEFrw4ai4N/ns/2/y/0ewy9X3gYo4bNQcLVFHz17Xh07d4lax598eVOzJ/3peAnwyJEAoPeG1X8PNH7+ccxZfLQXKcOlS3UG7SYOvEbLJi7XqQeZGSkCfGlUaNGmL/kdWxc9xMWL96Ml/o/haVfT4BapceeXYcw7r0FOHf2Ekwmh8s+eVGMfr8vdDqtiCYhkcLXT4f1W2ehfYfWgtmoYdNx/fp1YYb50KN34ZNPJwgBINOUiWkh32Dd6h+RcDVJGEhaLCaoVTpUrVoVdevWxfQ5b+Le+1tm9Z3m86SJX2Drpl1ISk6ELNsdc9zoBV+fqmjWvCGW/O9t8Vo+mEBFIFBQAUClUonQ/oyMDNrdFzv8gYGBUkxMTJYhoJv5n9SkSRM5KipK7tOnT/GZAMoOZxvxBkk/U8PCw8P1rVq1one82hVhcLgPTIAJMAEmwAQqEwEWAG492rTAopJpx46eRHxcAtLSMkRVD9qxpB3OBvWoTNmNHf6YmBjEXLwuFooNm9QQJenyU6qMzqEFGIVRX758WeSD025o/QZ14ePjKXb/aWFOz18eHlpcunQFpyMvIeHaFbFr37xFYzRqXBfpaWZRho8q1Xh66XDvfXchPT0d0ReuCWd2nypatGjZVLTJlCHjzJkzzs6r0ao1laJzWj6pLCKn/p+D4biWkCYiEEjwaNO2JZIS03D27HlIkhUtg5uLSAPqrwhfDw8X7acFYvXq1dGmdXtUreaJyJNnEHH8tOhHcKtmaNqsASxmGZ7eKsTGxAtX+vi4RLGg9/PzReOmQWjYKBAWk9rpjm/DubMxwjCOdvRr1Q5AUFBtyLDDZiUjRQ9hVHjqVJS4f2pKuhBFfH190bhJPQTVrQWLWRLtokVyQkICok5Fw65RwcfbG61a1IXNQpZ4apitFpyLjkVqWhq8dAY0bNgQXj60oLXdJAAQPHHN/18NICPdjBMRp8Vino6AmlXRoVMzIUgc/++8iG5o0DAwm6Gd0cOA6Oh4MQ7UJhoXWjDTPYOCgiDJuUeP0Pyk19CcjDh+DrGxl4SQRGPSvsOdgv3pUxeEQSSV2CMWVL2BFvcXzsfi5MkopFxPhbePF+o3qIXWrRsiLT0TRw7TLny8ow8d2wphiMrxnT93UZSPpAV+YFANtGzZEnryh5Bt8DTqcerURcTExCIjIwOZGVZ4eRtQq2YdtGwZDA+jBFNmdjGM5hWJTVevXkVKSoqYH35+fiJKoEnT+tDrPTj3vzJ9GFXwvhZUAMjMzBQCgHP3/6YqAIrrf9u2baWEhAQ5NjZW8QIomSoA9GajmADKsqwHwAJABZ+03D0mwASYABOomARYALj1uNplEzQaHXRao1gcUwS4xSILN367XYLNbXefFoKuB9Vup8VZXgeNgV6vFSHtrgftvtOinzwFaANIlJ5V04JPEgIB/UzLVkeZQsfiitrg3KgRi14KW9fqHItfyu2ma1HkgCxpxD0dpeXUYn/JbHbch8QGgBbLVJdPB6hsgKxFZpoaGi2g1dtEnXv39Ab3/tvs6cLLQKvRidfRNSXa6TURO+JpFYt3tVoHO90aEOeRuEARASqVUexm0/1p0Upf1FarhZjIkCk5g8rtqdUONs4vhaFKbRUMJDv1UWxeCYNGEnU0Gtpzl4RZIax2yKLeoxoqsu6nGvZ0ngSxKCUDRLq2g+GNgxbTdDgjY4WXQ/bxN4vqCSSQaLQqZGZQNEX2g5jRORQxIfrv/NlVWLrl/HFsyInxp2lG4objcHgTmU3kL6ET/0dNt0uOUpbUJ29vTzGmt2yvRg1JtsJuU0Gya8Rc02glaDWOdYgkq2AxO+Z29jFwzlMV/d4GuyTDZlXDLqVDJZYMNw6r2QIfHy+otW5mN5JKRFyIseCDCVQQAoURABQTQNcUAHrL8fPzk5KTk93N/6SuXbvKP/74Y/FEADh3+2UlAsD5xy7q3HAEQAWZldwNJsAEmAATqJQEWAAoG8OeU6SAI9uyFA5hSKoUfKIfHQvI/EQvlELrysAtHOX5cjqUMXKwcjcFzv4adzGIhIXcjrzEI7tTmMg2Ts6xcwzgbV5AO4Uj0RaVDRpVdgGA51gZmNrchFIjUBgB4FZlAJ1vNjcJAMr5xZoCoJS0dfUA4AiAUps3fCMmwASYABNgAsVOgAWAYkdaqAsqodyuLy75BTjtetMusBL677Igpd1wNde5dYyHQwC41YL91kJN7oJAXgJPXuMvEoMdvlxlU6whMUJEkFiFGEGGf3wwgcpKoDgFAIoAUKvVUmJioqv7f8mkADgjAcSbjJICwBEAlXUac7+ZABNgAkygIhBgAaAijGJh++Dc2aadYrFzrHxR6L/yu8JeuyK9zsEptwW5WMy777i77saXAA5FnlFEgOy3oN3/21ymWEQA6J0pJDqoRJ0GPphA5SRQnAKAewRAnTp1hAeAYgIYHBxcfCaAJBErbzIhISGqkJAQTgGonHOYe80EmAATYAIVhAALABVkIAvVDYcvwI3Fvs0hBFBUAH2V8AK2UE2+LS/KWwAQzcoj5L64Iypc4zNuiBOuYf+3WwCw3BAAJG+o1bmbGt6WoeWbMoFSIlASAkDz5s3lyMjIbKkAwcH/j73zgKuy+v/453LhshWRIeBARFFQ3CN3y7K0bNiyZVvbpZVlCS2zaVmmlf5alg3blma5M2caIi5UVETZm8u8z/9/DvehCzLvkMvlc3z5Yj3PGe9znnvv+ZzviFQSEhKsKwCojCgAnKPVwmZIgARIgARIwIYEKADYEC6rJgESIAESIAEA5goAIgCgkBcLCwtlGkCNRqMkJyer5lqKyAIQFxdn6gpgnSCAJpFhpPMOXQC4jkmABEiABEjAMQhQAHCMeeQoSIAESIAE7JeAuQKASSrAqk1/XS4ANgkCyCwA9ruo2DMSIAESIAESMIcABQBzqPEeEiABEiABEmg8AXMFAGMKQPWE39CxY8dqFgBCDFBjAJhkDbCuCwBjADR+onklCZAACZAACdg7AQoA9j5D7B8JkAAJkEBLJ2COAODh4aEUFRUpRhGgTgsA48a/KgigVdMAMgtAS1967D8JkAAJkAAJVCdAAYArggRIgARIgARsS8AcAUA90a8pAIg0gDk5OdWC/xndAmS8AAoAtp1L1k4CJEACJEACLZoABYAWPX3sPAmQAAmQQAsgYE0BoGYMANUCwKouAMaTf6EoyDSAxqLRaDQGRREJPpEEIKgFsGcXSYAESIAESIAETAhQAOByIAESIAESIAHbErBUABD77oKCggZjADANoG3nkbWTAAmQAAmQQIsnQAGgxU8hB0ACJEACJGDnBCwRAExO+E3N/g1+fn5KcHCwTAM4duxYw/r166ULgEAhU/dZWEQdimkMAHH8L8wP4uPjdVFRUbQAsBAwbycBEiABEiCB5iBAAaA5qLNNEiABEiCB1kTAFgKA2J9HR0dLAcA0BoBVBQBTF4DY2FhNTEwMBYDWtHI5VhIgARIgAYsIVFRUQKvVWlSHtW+mAGBtoqyPBEiABEiABKoTMEcAULMAqBYAISEhBo1GU5UGUFgAZGRk1AwGaDsLAAoAXNYkQAIkQAIk0PIJUABo+XPIEZAACZAACdg3AXMEAJOgfuoJv4wBIGIB1MwCIFwAkpOTlcTEROsKADXcCRzGBaCkpAQFBQUoLi6Gk5MThHdDeXk5DAaD/Ln+0tDf7XsxsnckQAIkQALnjoCnpzvatWt37hpsREsUABoBiZeQAAmQAAmQgAUErCUA1BEPoMoFQAQBjIqKsn4MADH2mJgYh3EBKCoqxm+/rsaZM6lwd/eEpirRQeUsCyGg9sLNvwXPAW8lARIggVZHoN/Anhg4cKBdjZsCgF1NBztDAiRAAiTggATMFQA8PT2VwsLCKguAxggACQkJ1hcAxIcFR3IBKCgowprf1yLtTCZ0Oh2gOAGaujb9xhUprmEhARIgARIggSYQiOrbFUOGDGnCHba/lAKA7RmzBRIgARIggdZNwFwBwMQNQGxOq2UBMBEDbBsEUEydowkA2dm5+GP1WqSmZkrzf1GcpBWAQQoBZ7kBcPPfup9gjp4ESIAEzCQwZOQA9O/f38y7bXMbBQDbcGWtJEACJEACJKASsJYAUFsMgODgYEWn0xmSkpJkGsDJkydbzwLAUWMAlJVVYMfWXcjPL4RW6wInpXLjb0yjWMvKtUZmRT4QJEACJEACrY1AaPfOCAsLs6thUwCwq+lgZ0iABEiABByQgCUCgNENoFEWACIGgMBnjd2qqENUVlWXI8UAqNvH3wFXH4dEAiRAAiTQrAQaDi57brtHAeDc8mZrJEACJEACrY+AOQKAmgawMQKAyAKwfv1621gAiA8KwkzekQSA1rcEOWISIAESIAESqCRAAYArgQRIgARIgARsS8AcAaCuNIDJyck1rQEUUwFAjIQWALadT9ZOAiRAAiRAAi2WAAWAFjt17DgJkAAJkEALIWAtAaChLACqD7tVBQDVAsAoLBgURdEBSAIQ1EL4s5skQAIkQAIkQAJGAhQAuBRIgARIgARIwLYEzBEAaroAhISEGE6dOlWVEtDPz0/JyMgwzQwgXQCsYgGgKIpGo9FUxQBwtCwAtp1u1k4CJEACJEAC9kuAAoD9zg17RgIkQAIk4BgEzBEAVBeAxsQAUFMEiiCACQkJ1nMBMAoBchYYA8AxFiNHQQIkQAIk0LoJUABo3fPP0ZMACZAACdiegCUCQB1m/1Un/yINYEpKivqzdS0AhAAg8IgggHQBsP1CYQskQAIkQAIkYGsCFABsTZj1kwAJkAAJtHYC5ggAqgtATQHAx8fHkJOTU9P03xAeHq7odDrbWQBQAGjty5jjJwESIAEScAQCFAAcYRY5BhIgARIgAXsmYI4AUFsWgLqCAKpZAIQLQFRUlPVdABgDwJ6XF/tGAiRAAiRAAo0nQAGg8ax4JQmQAAmQAAmYQ8DWAoCIASAsABITE5XIyEjrCwBi0IwBYM7U8x4SIAESIAESsC8CFADsaz7YGxIgARIgAccjYKkA4OXlZSgoKDA1+zfUlgXA6kEAhdk/0wA63oLkiEiABEiABFovAQoArXfuOXISIAESIIFzQ8BSAcAY5b9KAPD19TVkZWWpKQFNvyqTJ0+2rgWAQGQaBDA+Pl4XFRWVBCDo3OBjKyRAAiRAAiRAAtYiQAHAWiRZDwmQAAmQAAnUTsAcAaC2IIAdO3ZUkpOThRBwVhBAER/AajEAjOn/hLJQZQFAFwAubxIgARIgARJo+QQoALT8OeQISIAESIAE7JuAOQKA2NB7enqKPbhSWFhYc9NvWwHAGPG/mgCgZgGgBYB9Lzb2jgRIgARIgATqI0ABgOuDBEiABEiABGxLwFwBQBUBVAFAWACIWAC1pQFUswZYxQWgpgWAwKOp9AMwKIqiA0AXANuuGdZOAiRAAiRAAjYhQAGgbqziA1tFRQXKyw3yq9EFss4bXF1dUVJSIv9u+r1NJs4Klep0OhgMlWMTX0VxcnKCVquVX/Pz8+U4WEiABEiABCwjYK4AYGoB4O3tbcjPz5f+/iIGgL+/v3Lw4MEqSwCbZAEwnvrL0TuSC0BZWZllM8q7SYAESIAESKCRBFxcXBp55bm5jAJA3Zw3rd+N4uJSnH/RQLi56aQQUF9ZtXIzZsdMg1NpJD77cj76DghFcXGlIGBv5fixM7juylm46sZIxMbORl5+HjRwg3cbZ6z8aRvmvhKDWc88gssuG2/VMYgPwXq9fTEpKCjAXXfdhZEjR+KBBx6wt6lif0iABByAQFMFAI1GYygqKpIuAIWFhWqQP0NtFgDBwcFKSkqKTAOo0+mUhIQEy4MAOnoMAH1RGbZs2YbUM+k2UbqFqQQLCZAACZAACfSMCoXIz2tPhQJA3bPxyIOzsWnTX/hz/fcIDPRHib4U0DrBWauRJ+SKQVt5cq4pl9+PGjwNeYVJePixO3HZhFEICGwvT9PFf1EqrQnK5T3ypN3JKAZpSgFNBaBoAWlYKY5cDPI+cb24T1zvrHWFk1ZBRUUZysu0UFAKDZzh7KyFi4sTyitKoNFoUV6mgcFQAQOKoHVyh7MzoNW6wFDhhAqlEIYKZ6SeycLcF9/H0GHRuOPuicjLK4GrqztcXV3w9Mx3UV5ehudeuBcajSItH8RXJydnaBRXaJwAjVYPGNwhDlEUVLeOEH0FyqGBOzROFbJ/4rPQ4cMncOjQIQwY0A++fm5QDDrodOJaoKREjNEAKO6ApvJgxlChgda5HFpnDQwVqByzokArx6OgvBxw1uogmjMo5Sgrq4BiEPco0DpVWmO46LSy/+VlCqA4w9UNsv9lpQaUVxTLa/NySjFs6FhMmnQlXpn3vBy7Ogf29KyyLyRAAi2XQFMFAL1eL0/2a4sB4OPjU6sLgGoBIN9CLEVlKgCodakWAI7gApCbU4j16zfg1KnTcHG2vqmb1lD55sZCAiRAAiTQuglED+6BQYMG2RUECgD1CAAPPY1NG//G+o2/wM1dh5TkTISFB+F0SgYOJBxH+wBXhHYJQ5u2Hjh+/CTOH/IEekX74LOvXoCPjx+EtUdOTg727t2LrKwsdOnSBf369UNpaanclB49chxt2/jD3d0de+N3I6JnmNy06lzc4OXljX92xaG0REH/gT3Rtp0OWzb/W/nzgF5o6+MJQ4UWBkWPlFMZiNuTBBedggGDeiIgwBelpeXQOrlBQTnEadCJY3nwD/BCn/4h8HBrh7zcEiSfPAMPTx1Cu3ZCaUkFNE7l2LEtHrt27Megod0R3bc3XHUe0DgZkJqaKgWLdu3a4cSJUzh86Bj6REegQ4dguWEX68i0aJzKUFaq4O8t/yDlRDH6DuyE9xZ8hJ27tuO7779CWLdApGekY++ek8jKzEOffl0QGhoKxeCEgoJ85GQXwi/QFSXFZYj797Dc6PfrH4U2bbzl2ERfk09kYn9CIry83dEzsosUaYSVRkZ6FhTFCW66tti58x9ERnVDpy5+OHMmDQf3n0BaWjp6RYahc+fOcPcEcrJKMGzwOFxz7RV4ae5sCgB29QrFzpCAYxAwRwBQswAYrQCqggCaCgDR0dGGuLg426YBFG9Y4kU+NjZWExMTY3CEIIBCANiwYWOlAGAD00wKAI7x4HIUJEACJGApgf5Do+QG0J4KBYD6BIBZ2LRxKzZvWY209NN44K4FCAsLxpa/9iL5ZDq82xXg7mmT8eycR3HBiLuxY+sRaDQV6BXVFV+seB6nk0vw8ANPo0TvgpKSMuhL0nDHPVfhiaemwbuNGx5/9DmknSmQJ/c//rQcmzdvwXffrsKeXUdQVgrs2n4EcCrEg49diwPxWfj5x3Xw8tZi4pUX4pU3H5RCwZKPvsA7b3yFdu18UVysh4+vC2Jfvg8XXTwKGemFeOyhF7F2zT/w9+uA1LRkXHvjGLw8bwZST+fhztuewojRUXjplZk4kpiCRx+Kxe5dBxAS3AkZmWno0ycKT8+5E4OHRCPmmcXYtHG7rGfH1oPIyMjAkBGdMXvOQxh6XnQ1AUDEFkg8fAwP3TsfKSkpGDKsDzZv3I1ypGHZF0sxbERP7NyxF088+hrOnCqGu4crcnJTcfOt12DOC/fi3z2HMPOxF+Hl6YPTp/Lxb9x+tPNxw8xZU/HYE7ehIL8YX3/1E+a99CHKSnRSLPHwdMKC9+dg8JAIPP/cx9i14yA0zrn48dePsXz5J+gXPRS3TnkY2Zml8Pb2QFFxFu6ddgvuvGcyivUVUgC4/oZJiH1xprS6kNYYwiqDhQRIgASsQMAcAUAN6mf8elYWAD8/PyUjI+OsGABWCQJomgXAZPwOEwQwL68AG9ZvQkqKjQSA6qK4FZYQqyABEiABEmiJBKIH9kb//v3tqusUABoWALZs/RPp6WfQO+J2dPTzxeyX70RAYDssXvw+kpJO4NffvkdCwkFMv2sufNr645U3HsbQkZ0Q0W0k2nqG47U3n0FIxw746afvMPfF9zBv3jw8PPMaPHDfC/hw8QpMvv58XH3dKJw/dhweffBlfPXlj3jk0ekYOeo8vDP/I2zYsB23Th2PSVddjk//9y2+/f5j7PhnNfx8u+DiMfdg0JBIvPb2/fhn50Fcc8XDeHTmTZj76pN47ukFWPD2R3h2zgxcdMkQfPn5b3jr9U+x7KvX0K9/L4wYMhkXXToQn3/5DqZc8yJ+/e0nvPb2g+gb3R8H9h/BPVPnYPJNI/HJsrcwbep8fPzxp7j+hqtx3XXX4fjJA3jw4bsw57k5mPHkfdUCJAoXh7ff/B/efPNNLP/yG4wYE4kP3v8aTzz+Bt5+53nc9+B4jDnvDhw/loZPl7+MLqFBeP/dZfjfJ4uwecvvyMspw8XnX4dAvwg8MuNWdOrSHq/OXYSjiaex9q+PoNcX4fJxd6Jv3/5YsHAuTp06hfvvexKebYvw15Y/8Oj9C7B40ccYe8FgTLp2FC68eBiWfvAzVny9Bgs/eAlh3TrigfticOjwXqzf/C1cnN0xbMhFuOnmSXgu5lHpWkABwK5eptgZEmjxBMwVABoTA0AVCKzqAlBTAHA0CwBhLpaXW4Di4mI4C0c5KxcN6g8aZOXmWB0JkAAJkICdEnD3FKbdXnbVOwoA9QkAT8pTb1UAiIy4DffefBkWfPokFA3w8dL/4emnXsbvq/9ARGQIorrejLDwAKxavwA7tyfgggvG4O35i3DnPdfAYChH6pk8DO57lQwq+NmX8zDtnhj8sWon1mz4GKGhQagwVOCma2bjwIGD2LzjE3i6e+O5pz/C/DcXI/7gaoSG+eK7bzbhuhsmYMu23zBk8HBpmp+VWYjS8mykn67ATZOfwK13XoTX35qNi8fcKV0QVv7+Efz8vZGZUYAPF32FCy4agXbt2mDiuEcwZHg3fP7Vy+gU0gd9oy7A9ysXwMVFK33up9x4Lw4nJmDr9g2Y+cgbWL5sJdZsWILefbqhsKgQAX5dcMuNMzDvrWnQ6dyqQApr0ZdfeAdfLv8U8ft2w9VNwZYtO3HxqAfx0isz8MCjkxHWNRzjzr8D0x68Gq5uLti1/SCm3T8Vixd/gH79+uLyS2/C40/ejocevlvGNYh99l289/a3WLV2MY4cOYK77ngEr73+PEaO6Y3s7Hx8+P4P2Lj5NyTs/wfzXlyGeXPnY/u/XyMyqrt0qxCf8fJySpCVcxrOWnfMn/cz1vz5PTZv+wbeXu0waMAFuPa68Xjx5Scqgz0qLrQAsKtXKnaGBFo2AWsJAHVYA1S5AERGRtouCKBRFHAIF4Cq5WSrk3qLozC07AXP3pMACZAACdgvAQoAjRAA/l6H9IwURETcjhcfn4KZ8+6QAfrqEwDWrtkOYYb5wYfvY8KVo2QAPnGyfelFU9B/UDd8/NmbuOv2Z/DXpjj8seETdOgQIF0Bbr8pBgcPHMbav5aijXcbPPPEYnyw6DPsiv8GoV2D8M3yP3HjTVdj287fEdkrGove+wIfvLsKiqYQ7rpgHDp0AA/OuAKvvDYDY4bdg4K8Mvz0zBBTZwAAIABJREFU+6to09ZLxgTw8BSHEi6IjzuCSZc9hGEjovD5V6+ia2gPjBl+E95Y8DB0OmdAccULz7+C7777Drv+2YJnn1qIb5avwZpN76B3n+4oLCyEv19n3HjNDLzx7v0ygKBaxAfdDeu34vILZqF3dGeMGz8MO7btg74sGV99swQa6DBy5GgUZATDP8BHWg8Inp27BOHhx6egnW8bXHLRNZj13F146OH7ZDCr2OcW4v0FX+GX3xdix9bDmPHIPHTu1A0uriLYoCvatvWU8Qk+/3Y2XnvpK7z+6ifYHvdBlQCwecMevPLCJzh8+DB8fN2RmQq4ehRh/ZZl8Pb2xqB+43DtdZfjxbkzjC4AzjIQIwsJkAAJWIOAOQKAGgPAdNMvsgAkJydXcwdQswCoLgMi2LA1tp+iDrE9Nq3LYVwArDGp9dfBNxDbM2YLJEACJGD/BETA+MoI6fZTKADUIwA8PBObNuzEFikAnEZExG2IeeB6PLtgurzpf0uX1mkBcPjgCQwZMgQvvvAGpj8wBVpnA04kZSK868W4847r8f6SWZh6ywxs37off278FP7+7VFSWow7b4nFoYOJ0tTd27stnnxkEZZ+9CV27fsCoV1D8OVnv+OW2yZj1+6N8mR7+NAb8M7b8zD9oUlIOpqFnt3G49GZ10sXgPEXPCAtBP7c/AHatWuLjIwcxD67EBePG42o3uG4cPTNGDl6ID5f/hpCOw9Ap6BorNm4UMZDUgwuuOn6+3Hi5DFs2fYrHrjnVXy/YjXWbFxkFAD0aO/rj5uvexZvvjcNLi7/BVEWmQGWfPANFr3zq3RtyMk0YOzFUbjy6jHQOlegIK8Cw84bjMsuvgPzRCwDnQ6pqenyhH/iVcOkQDBx/G14avY9eOiRO6GBi7QAWLTgR6xa9z6OJR3AbbdMwy+//IpRY3vBYHDCmtVbsHnDXjw953a8MHsZ5r+5rJoAcP7oiXDXBeHr799Gez8vTLl6Hv76ey12xn0nx1spAEzEi3MfowBgPy9P7AkJOAwBcwSA2mIA1CYAmAgEitUsAFQXAGM2AIcLAugwK4sDIQESIAESIIEmEqAA0HgBoEfETXj2vhswZ+EDcNI41SsAiM157+4T4ebmhtkx96N796745OMvsPSDn7Dkk9dww81jcMets7B1yz6s3fQZOnRoD31xCW67YY48pd647WN4enpjxoPvSwuA+ERhARCCzz/+DbdPnYI9ezfjTOoxXHLR7XgxZj6uuGo0Fr+/DAsXLcLEyy/D0k/exGefLcOjjz6F2OfmYdz4wfh+xRq88+bn+OSLV2XgvuGDrsOYC/ri8+Vv48Fpz+N/S77Gc3OewvkXDcC2v/fhqRmv4oYpl+KjT17AvVPn4qcff8GaDUvRu08ECgv18GkfjJsmPY6FSx6XqfiASnFLWAfce/tc7Ni+By+8OhVC+BLm90X6XFxwwUUYel5PTLz0fuzacQBvvzdbWjZ8+vG3WPHdt/j6m8/h7u6Gy8bdhqfnTMVDD98Lzf//i33uXcx//Uts3PoZ3NydcN7A69AtLBLvvP8ksrIz8OSj70BfcRCHj+zBk48uxoL5y7E97qMqC4CoqCh06TAGsS89jPy8Yky9/R4Yyjzx1Q/zMGDAgBoCQGllOkZaADTx1YSXkwAJ1EXAUgHAy8vLUFBQUBXwT2RKNd34i5/VGAA2CwKopgF0hCwAXKokQAIkQAIk0FoJUACoe+Znz3oFO7eexI+/zUdmZg6uGPck7r1/EqY9fDk0cMbyL37F6698hK9/mI9OnTph4sUzEBrWHgsWz4Krqyu2/30Izzz1Eo4cTpM57TVaPR545CbcN/1mmbrukelvYf++JHyy/Fn4tPOWHXnqsYXyd9/8/CLcXD0x/42l+OWXVfhi+SJ07OSHn37YhPvuvR8rV32J4OBgTLsrBuv/jIOHpxaTrjkf7m7eeO+99/Hq63Nw+92XYOaj8/DjtzvkKbfwg5/2wA0yS8Chg8dwx5S5iO7bC28vmo7kk2l48fl38OeqffIEvLRYg6uuvQBPPnszOncJxAvP/g+rf92FT7+ZgfDwcGl9EBF6KW6+ZTKemH2zHK9aRJrDN1/7AIvfXYnefcJRUpYj/7T332Pw9fXBL78vQvv2Prhv6mv4+6+98m/CDWD287fj7mmTsG/vUdw8eQ5mPn0Tbr/rcvn3t9/4Woofi//3FAYMisDnn6zC3NjPkJGeK2MWdAhqj3lv3Y8Lxw3G63OX4ZMlv+Kbn2MR3qMjKioqsGTRL3jjlS+QlZWHqN5hmHrPBMx74Ut06hyABYsfx+03voDxE86TfZAxAFhIgARIwIoEzBUARBBAsdEvLCw8KwuAKgCoLgA2DQJoZEEXACsuClZFAiRAAiRAAs1BgAJA3dTT0tLkxrZNmzYQm9qCggJ4eHjIU33BLT8/X26EAwICZCXq9W3btpU/i3sLC0pxJPEEMjMz5Ql+17BglJXrUVpiQEF+ibxO3fyL73Oy86t+JwITizb0ej18fHzkJl78nJubK9sU9ZcUl2HfvgS5aR8+fAiKisoR9288OnTogJCOAdBonHDo4FGkZ6TCx6cN+vWPgl5fKOMN5GTr4ebqIX3nRREn9QcPHkJmRjZ8fX0R1SdMbobF5lz0S19cADE2dbNfc7wqyX///RdXXXUVXn1lAW65bRLKykvg4uyMt9/8HI8//ia+Wr4A11w3FoVFehw6cBIFBYXw9/dF94iOkqtI85eTkw8PD1e08/WUfRApm8UY2/p4SpcBselPPpmJ40kpcHNzRWTvrvD0dIdeX4KC/CIUF5fAt703tFoNKioUuLnpcCQxGceTzqBXZFcIMeXwoRRZb/cenZGXly/5tvdrQwGgOV6I2CYJODgBcwUAEzcAKQAIFwBhCZCTk2NbCwCj6X9VDADx4qwR7waAYwUBdPCFx+GRAAmQAAmQQE0CFABsuyZEcDvxXxRxEi02sSJNnogFUflRqu5i/LxVdZ16vfpViBLie7EhFnWKusUmVrQn/qa2I/7u5KRBeXmF/L0adE/0R3xfs97/flbk5ln0oynl6NGjuGvqIygv8UDnTuHQOov7hRBxBNH9O+ONt2fLwHvOztXjYdQ8eddqFSlKiHGIPghBROUn+ujsrIOrq4vsWklJ2VldFJt/cb0Yg3BREPcL7pXzYDirfXXc6njFfSwkQAIkYA0C5ggAIgigRqMRp/8yyn9ISIjh1KlTasR/g5+fn5KRkVFTCJAvXDYNAkgXgIaXBN8+GmbEK0iABEigNRKwxhu0pdwoAFhKkPfXJirtjz+Jdes2GdPplQEGV5mJQAT5a+fjJ4MiNmRqL8QKcXIvNu9ig6+KFf+JFtYNqCmEBmFZoG78myp8cCWQAAmQQF0EzBEAmhoEULgA6HQ666QBVIMA1pYFgAIAFzoJkAAJkAAJtFwCFABa7tzZe8/FB15xgq9upMWJfGmpOJGvsPeus38kQAIkYFUC5ggAahpAEQegvhgANYIBWscCQHUBEF+lScH/m6wxCKBV1wQrIwESIAESIIFmIUABoFmwO0SjqouCQwyGgyABEiABGxIwRwBQLQDqEgDqcgGwWRYAozUAYwDYcKGwahIgARIgARKwNQEKALYm7Lj1C396YTbPQgIkQAIkUD8BawgAagwAHx+fs4IAjh071pCcnKwkJiYqFAC4GkmABEiABEiABOokQAGAi8NcAiK4oPDTZyEBEiABErCdAGBi4m8a8E9+Hx0dbYiLi1MDA8qUgaIn1ogxJOpQ6ALApU0CJNBkAoyC2WRkvMGBCVjjHdnKeCgAWBkoqyMBEiABEiCBGgTMtQDw9/dX0tPTqyL/q2KAr6+vISsrS/4+ODhYSUlJUcUB61sAmPh7OWAaQJqx8Wl1RALWjZLsiIQ4JhJozQQoALTm2efYSYAESIAEzgUBcwUAseEPCAhQ0tLSxEb1LAuAGr+TFgBWdwFQI7nGxsZqYmJiHDcGAE8tz8WzwDbOBQENha1zgZltkEDjCNifIEcBoHEzx6tIgARIgARIwFwClggApi4Abdq0UfLy8gymFgA2yQKgpgE0ZgOQ43boLADc/Ju7tnmfPRKwQ5Nje8TEPpFAayVAAaC1zjzHTQIkQAIkcK4ImCMAGKP8q379tVoA1BYDwKoWAKYxABwpC0BZWQWOHTuGvNwCiIA2pvslJ+W//LXnaoGwHRKwLoHGKQAGDZUv63JnbSRwNgG/gHYIDg62KzQUAOxqOtgZEiABEiABByRgjgCgpgGszQLA1PRfjQEQHh6u6HQ6JSoqynpBAMWm3xFjAOTk5GH9uk1ITk6pFACM+yCx+RdFdXtwwLXIIbUCAhrFuZZRnu0WoNSrE9CNoBUsFQ7xHBDoN6Q3Bg0adA5aanwTFAAaz4pXkgAJkAAJkIA5BKwlANSREcD2WQDEoB0pBkBhQQn++GMdUk6dgZubBwUAc1Y177FbAhrTnX09p/xKo2MFNFUMsD+fZ7udLHbM4Qn0GxKJvn372tU4KQDY1XSwMyRAAiRAAg5I4FwIAMICIDEx0TpBAI2+/0JZkBYAomg0GofJAlCQX4zVq/9Ayqk06HSuFAAc8KHjkGoSONvc36BaADRKCGi8COCkUADg+iMBlUD/oVG0AOByIAESIAESIIFWRuBcCADCOiAyMlJJSEiwrguAyVw5jABQXFyMQ4cOoaSkpFLcUF0AWtnC5HAdk4ABtbkAnD1WW0UAqGaB4JiIOSoSaDSBDiEB8PdvD1dX10bfY+sLaQFQO2GDwQAnJwqYtl5/rJ8ESIAEWgMBcwSAxgQBNI0FMH78eOW3336zjgWAmgXA+FX6xKsWAIqi6AAkAQhqqZMn3uRNiyoACIVDmj3wA0BLnVr2WypaxEACJEACdROgAFA7G5OYR1w+JEACJEACJGARAXMEADUIYEBAgJKWliazAKhpAMX3RoFA/N72MQCE9b/DpwFUd/8WTTVvJgE7INAok35b9pMnaLaky7pJwFICFABqJyg+6zAIsKWri/eTAAmQAAkIAuYIAKYWAF5eXoaCggLTzX7Njb9BtQCwyvmfaQwAdQopAHAxk0ALIUABoIVMFLtJAs1DgAJA7dyF+X9NC8HmmSG2SgIkQAIk0NIJmCMA1JYGUD3t9/X1NWRlZcmTfzUNoE1iABiFAJW/jAHgCC4AdS4oaf/f0pcb+08CJEACJEACdROgAFA3m4ry6tFRhFWARuYINghXSAC0cOKzRQIkQAIk0DABawoAPj4+hpycnLMsAFTBIDIy0ipbWGkQbyoAOLQFQMNzyCtIgARIgARIwCEIUACofRpF+B+da+UGX7oCKE4oKSmDCBukGNOmaJ0tOyUQFgaibvW/i4uLQ6wpDoIESIAESKA6AWsKAMbAf3UKAJMnT7aeAKCmAXT4GABcsSRAAiRAAiTQSghQAKh9ovNyC/DX5p3IzMhBcXE5uvfojOEj+6JNGy+UlwPF+lI4aQGtVgtnZ2dotRoc3H8C+xOScMllQxu1ejTQws3NGWdSU7Bz1zaMHnU+PD18IMSHktLSs2IQuLhoZb1lZRUw/V5tTPxO/E0tOp0O5eXlEJYM69f+g779w9EhqD0qKirkf9Pi6lopPvyz8yCys/Jw4bjBUvDYvGEPAjv4ontE50aNiReRAAmQAAmcTcBaAoBpEMAawf9UQUCkArSuAKAOhxYAXNok0DII2Cq9X2NHb9n5WGNb4XUkQALmEqAAcDY5sZH++684rPj2F1x59RgU68sQHByEyN5hSDx0GuVlQI+ILoDGgLzcQiQePonovj0QvzcRq1duw/VTLpKVdg4NlF9PJKVW+1lt0VChweFDx6WAsGnzOtw0ZbJ0LdifcBCdO3eBTzvvqs4JwaGkpBTFxaXo2Mlfig2ihIWHSDEgIz0XySfT0C28I7zbuCM/T4/Dh06iS2gHtPNtg0ULvsf4CefBz7+tTG8ohIuCgkIEBLZDWmq2vDeqTxhSz2RBX1SCiF6dceJ4Kjat342LLx2Ktj5esr3cnAKkpWZREDD3geN9JEACrZKAOQKAGgTQNAuAyaa/mgVAaGiowdnZWUlMTLR+GkA1LQ4FgFa5djloEiABEiABByNAAaD6hAoebm46bN64G3+u/gcPz7geFRUGuHvosGrlFhw7mgIXl8oMAeMnjMKXn/0uN9W//fI37p42Cd9/swEjRvfGF5/+gZdeuw+ZGfn4a8MB+AXqkJ1ZhLmvT4O+WC/v/2jhD3JDnxB/DFF9QjH2wgFY8dV6hHULwd64REy57VIMG9FbdvCjhT9h3Z//4KZbx2H73/sg3AVEPAKxwe/StQN+/n4TOgT5Qq8vwxVXjcTyz/9AULA/ko6dxqWXD0N6WjY6dgrET99vwrQHr0Zubj7Wrt6HweeF4asvfkeHDv6yT9H9u6KkuALde3TDe2/9gL4DO+HokZOY9sANmH7nGxh32SBs37oX115/ESZdN7rep0FNq9xcj4xCBbq50LNdEiCBGgTMEQDqCwJYXxpAm7kAGMPjGeLj43VRUVFJAII40yRAAiRAAiRAAi2LAAWA/+ZLTfvn4eGGDet24vnZn+DCcQPlaXlwSHv8u/sQBgzugfZ+bbBx3R50DQtBebkB90y/UprYZ2bkYv++JDw95zZ89r9VckMtNvBxu5NQWqbHe29/gW27v0CRvgiJh05h0TvfYuHSmfj3n8P4d3cisjJzpRhwx70TsfLHv3DRJUMwamw/aDQK3n1rBTp29sf5Fw7E3be+giuuHgWts4KsjHxERXdF/L9H0aatJ8J7dEJaaia8vb1w4bhB+Hb5esTtSUTPyFCE9wjBp0t+w8xnpqAgX49VP+9GesZpRPTqhKl3T8T2rfuRdOyUtHBIPp6D3n1DcfmkwXjsgTcwfMRArPvjXzz34u04dvQEVq/cgSeevb1lLXb2lgRIgASaiYC5AoC/v7+Snp5eGX228r/8Xs0CEB0dbYiLi1P/Lr5axwLANA0gLQCaadWwWRIgARIgARKwAQEKANUFAMHD09Md69fuwKZ18bh7+hXSRD/lVBoWvbtCntK39/PCoYMnoS8ql/7xU269RLoBxMcdw9Ejp/Dw49fhi89+R3amHl3C2qG0xCA390uWfIJtu36Gohiwfes+LF6wAkuXPY3UM9nYvHEP4v89hlPJaVIAOHwwWZrhDx4aKYWEJYt+kpv7Pn274aH73pSn/O392uJ0SiaGnhdZ6UawYQ+EL7++qBR9+nXDkPMi8fuv27D1r33oHtERnbsE4afvN2DGrJuRk52PX37YgaysDHTtFog77r0SKcnp2LolTvb3339OYuJV52H46J545YWl6NatKw7uP4V7H5iII4kn8NfGvZj+yA31r0ilmbMkNHsaXBs8sKySBEigRRIwVwAQG3rVBcDb29uQn5+vCHP/pKQkg9FFoEoUCA8Ply4AApA1DKCqZQEwigAyDSAtAFrkGmSnSYAESIAESEASoABQtwCwbs0ePBM7FZXR+ivw6dKVSE/LRVCID1JOiY13b3z39Ub0HxiOP1bvxI23jMPuXQfx7PNT8fnHq6EvMiA3Lw35+YXw8fHFokXvYt2GlfIkPzc3Gy899zG6hnXAjm0H0CsqFIOH9pL3CZN9YWFww80XY/ioPnKOFr69Ap06d8DV14/B9DteQ2hYkIwXUFigR+/objh86ATatfPG4UPJmHTNGGnqH9o1CAcSjuPSCcNw6mQaRp/fDwve/FbW56LTwkXrhUsn9sdXy9bI9vVFZQiPCEJejh6dO3fGgje/w6TJg/Hrz1vw5Ow7sHjBL3jj3WnYs/sg1q3ZjVmxt9b7FFWmSmy+olp0NF8P2DIJkAAJVBKwRACow++/ziwAVhUAVDGBAgCXMgmQAAmQAAk4BgEKANXnUfAQQfLExrqgoAB+fn5SABA+9yKi/rZt2+TX/v37o71fO+yLO46TJ0+ie88gBAQEIDc3t+qrSB1YrK/A7t3/okePHtDrC9HWxxtdunSU/vs5mXnSEiAg0FdG5w8K9pOWBMKNQMQB6BH5X+T9tLQ0uLq6om3btigpKcHOnTuhVbwQ0asL2rb1xq4d+2U8gajeYQjp5CfrOHosWbbVvUeoNPVv3749MtJzEL/3IEJDu8HL2xVBQQGIj98vx9CvX7R0dxAWpgGBftixfY8cT+/eveHr64sTJ5IQEiLcHsqQm5eNwMDKIId1lebegDe3AOEYrxAcBQmQgDUImCMAiBN+jUZTqwuAqTtAcHCwkpKSYrCqBUBNFwCpKlS+qtICwBorgnWQAAmQAAmQQDMRoABwNnhVBKj5F2Fmb1rKysrkplwtYmNuWsRHJbGhFl/Ff1GvEBMqLQoU6Jwr0/rVVUrLq6fqM71OtKtBOcpKK68RJ/qimP6scXKGwQCZ8k+0KYqaAUCkGlRTBqobddOv4nvTsYmf1f9q/5t7g99MjwybJQESIIEmEzBHAKgZBFC4ALRt21ZJTk6W8QDoAtDkaeANJEACJEACJEACFABqXwON2dxaesLcUJT8hqLYG8orE72KNIDqZt70Zyfn5jXB59NFAiRAAiRQScAcAcA0DWBRUZGhoKDA1Oy/6vuBAwcqu3btUn+2TgwAUwsAk0mUFgCKougAMAsAVzcJkAAJkAAJtEACFACab9IcXQBojIhiS/qWCjS27BvrJgESaF0EzBEAaksD2LFjxyoLAPH3mlkAIiMjlYSEBOsFARSW/46YBUD48gmzPWEiJ8z7hLme+J34WZjKsZCAJQScnJwhUkqxkAAJkIA9EqAAUPesiA2k8P3XajXVTtjtcR7tsk/NnQXALqGwUyTg+ATEa2ZJaXWXKMcfdf0jNEcAMLUASEtLk2b/NQUA01gAqmAwefJk2woAjmABUFiox8pffkNKymm4uroLu4YqX7nmVq9b+8PiCON3ddfhykmXyaBQLCRAAiRgbwQoANQ9I/n5+TIQoDgUqOn/L+4SBwUsdRNQDHRB4PoggdZIwM3NDf4B7Vvj0OscszkCQG0WAKYZAc5JDADVAkCMzJGCABYUFGHN72uRdiYTOp0OImovmDuWD62VCGi0wOUTLoK/v7+VamQ1JEACJGA9AhQAqrMUm3rxWSA7OxtJSUnw9PSUwe8qo+OzNIkABYAm4eLFJOAoBPR6PVxcndG9e3dHGZLF47BEAAgICFBUCwAhAISGhhqSkpKqggCqWQBsYgFgjAUgAcTExGhiYmIcIgtATk4e/vx9HdLTs6XJv4zaKw0nDBQCLF7urEBx0mHCFRcjMJACAFcDCZCA/RGgAPDfnAgW4qS/tLQUBw4cQIcOHRASEgxFEdHz7W/u2CMSIAESsEcCJSVliIuLk9avtICtnCFrCgBGs38pAAQHBxvi4uJE4D/rBgEUB/5GRaHKlsuRBACRN3fzhi3IzMyGIkLuVpQbN/6V0XVZSMASAhVwwUXjRlEAsAQi7yUBErAZAQoA1QUA8ZMQABITE9GrVy94errLC4QIwNJUAoTWVGK8ngQcgYA4TD16NEm6UInXUeFG1dqLOQKAGgPA1Ozf1AJA/b2pBYAIAihYW+yAZZoFQPWJV10AHCEGgJobt7UvTI7fRgSMLiUMKGkjvqyWBEjAIgIUACgAWLSA6rlZY/EnUFv1jPWSAAnYmgAFgOqEzREAGooBYCIMVFkACAEgKirKcgGgNgsAoyjgEC4Atn4AWH8rJyAeSX4IauWLgMMnAfslQAGAAoCtVicFAFuRZb0kYP8EKADYRgBQswD4+voasrKyTE3/6QJg/48Fe9hqCFAAaDVTzYGSQEskQAGAAoCt1i0FAFuRZb0kYP8EKADYRgCozQXA1BJAWAAkJCRY5eyxzhgAjuACIKZHfAAS/iosJEACJEACJNCaCFAAqC4ACHet4uJiGQOgZ8+e8PLysOPlIA586iuMXGjHk8eukYBDE6AAYHsBoGYaQLGltYkAoG6UHSkIoEM/fRwcCZAACZAACdRDgAJASxYAuLRJgARIwD4JUACwXACoGQTQ29vbkJ+fr5r9V6UBrBELQJk8ebL1LAAcNQ2gfT427BUJkAAJkAAJ2J4ABQAKALZfZWyBBEigtRGgAGC5AGAaBNDLy8tQUFBgqC0GgMgCoNPpDElJSdICwCpBANUsAKoAYLQCEPbyDALY2p5mjpcESIAESMChCFAAoADgUAuagyEBErALAhQArCsAiH238X+VBUBtWQCMv7OeBYCIZU4XALt4ptgJEiABEiABErAKAQoA5gsAFRUV9c6BVqut9+8lJSVVf3d1dan6vqSkzPi9E0x/b1qZuMbZuX4f/4baF/WVlf03BheX+vtrlQXHSkiABFoFAQoA1hMAAgIClKKiomoWADXEANNsAOJ76woAJkOhBUCreHw5SBIgARIgAUcmQAHAfAHAnHWhbu4rN/ZNC+JX/V7RetPuN6e/vIcESIAEzCFAAcC6AkBaWpq0AFBdAGoTAMLDw5XExETbxAAQHxZiY2M1MTExdAEw54ngPSRAAiRAAiRgJwQoAJgvAOTnF+LHFRuxb28SLhw3EGMvHABnZ2cIywCRWWj71gQEBLRHWHgQFAUQyYbW/bFbNjh8VG8sX7YKBxKSMHJMNC6fOEL+Xmzyly/7Q/6+/4BIXHfThVUd3L3rEDLSc3HxpYPx9Rd/Yl/8YYSFh+Ca686Hl5cbiotLsWrlVuzYth89I0Nxy+2XV1tle/89Kn/u0zes6vcb1+3B6l+3oVdUKK6fchFoBWAnDya7QQItnAAFANsIAA2lAbSpC4DRsoACQAt/ONl9EiABEiCB1k2AAoB5AkBBQRHmvfAZUs9kY8wF/fDxR79i+sPXYNI1o7F2zU78sGIjVv2yDR98MgvnX9S/anO/+N0fMG78EGz7ex82rNuJMRf0x4cLf8Dd0yfhtjvGY9aM97Fp/R758++/7kLnLoGY+8Y0KQwIsaF7RCcwqKfxAAAgAElEQVS5+T9xPBUXXdpXbvh79OyEOS/ehXfe+Ap//r4LV183Bn9v3gcNdPjw06cgNv7LP1+DT5b8ivETzpO/E+Xzj1fjg4Xf457pV2HD2n/k7xYueYIiQOt+SeDoScAqBCgAWC4AiCwAGo1GSU9Pr/L7N7UAqC0NoNUEADUIYA13AukCoCiKDkASgCCrrBZWQgIkQAIkQAIkcM4IUAAwTwA4cfw0Jk+cjR9Xv4rADr7YtGE3xOb+k+XPISe7AKlnsvDqy5/g9rsux9gL+0MDDfb+ewLzX1uOF+bdjWsnPIOPv3wWPXoGYdOGPVixfAvuf/QKzH7yPdwz/VqMvbAftm/diyWLfsFHn85GyqksLHznW0y9ewKuuuwJvL90BkaMGITUtHTMevw93P/QFNwx5SW8t/R+jBzVH4mH0nHbTU/gq+/mw7e9N86cScXc2C+hLzTg02+eQGlpOV5/5X8I6xaCG2+5BGdOZ+Hdt77Fw4/fiIBA33O2/tgQCZCAYxKgAGCZAODp6WnIyMgQG3/5X80CYBL4T7gESGFAZAFISUlRf7Z9DAAKAI750HJUJEACJEACrYMABQDzBICDB5Lw6ovL8P7SmdLsPy01G+PPfww/r3kVHTsFoqysDE/PfBfjJwzH+RcNkALA2jV78ON3m/DKm9Nx580v4/V3HkBwSHuUl5djZP+Z+GjZI1j969+oMJRhwpXn4duv1qCdrw/uf+h6HE86g+++3oTpD1+NpKOn0bGzP7y9PfHrLxuxcd1uPPjY1Zhw0Uz8uvYtBAX5o7QUmHrzLEy7/zaMHNMbilKBl2O+wJHDqfjws0eRcioTi95bjq5hwdDp3JCXm4/ofhEYPbYfgPqDC7aOJ4OjJAESsIQABQDLBAC9Xl+1oRdBANUYADUFgOjoaENcXJztgwAyBoAljwPvJQESIAESIAH7IUABwDwB4O+/xOn8z3hn8WNwd3dFRnoOLhzxIFb++To6de4gTfYff/ANXHbFcFw2YTjKK8qxZNEqeHu744qrxuC+O+Zh3lv3I6SjH4qKinDx8Dn4+pcn8fWydfhy2Urcce9l+Hb5n7hl6mWYcut4/PTd33BycsKka0ZVhXf+7H+/4r35P2De/Htx3qgwDIqailVrP0BwSICs8/YpT2PKzdfiymtGynhRs2d+jGNHUrHsu1k4kpiMZ554V/Znym2XIT4uEVv/2o83332EFgD283iyJyTQYglQALBcADCa+FezADhnLgBG039FuAKIoYjANjExMQwC2GIfSXacBEiABEiABCoJUAAwTwAQFgBzZi2RZvw6nTPycoswdth0/LbuTQQF+0muj0x/HRMmjcDFlwxBaVkpHrjrHcyacyvc3HSYdsdrWLT0aQQEtkVpeQEmXvA8Zr94Pb758k9MufVynDeyF+L3HsKiBT/gmZg78drLX+LqyWNlwEBR9z23vYKcnFw8NftODBzSHdnZmRjW7y6s2fABOnfxh75YjymTn8T9D90mAxQKC4CYpz/F8WPpWLLscSQdO41XXlgiYw0MHNwTFeUKYmcvwSWXnYfRYwfw8SABEiABiwhQALBcAFDN/01P/evLAmByvfXTAIo3Ho1QAQAGAbTo0eDNJEACJEACJNC8BCgAmCcAiBgAky59Et//9iq6hHaAOI3fvGEv3vvocXlSX1ZWgWl3voRJ147G5VeMwNHEFNx7+xv4868FSE/LwSVjHsG7HzyB4aN64o/ft2P1yl0ymv+ihZ/jqWemoWdUEI4fS8V781fgxlsuxY/frce0h65Chw7t8NiD87EvPhFLPpuDoKBAFOvLZZtCgJgxawom3zgWu3bG4+5bXsfaLe+irY+XHOSsxxci+WS6FC1EpoK5L3yEDkHtcc/0STidkon5r34tsw4MGhIlRQZRJwsJkAAJmEOAAoDtBYCaQQBFGkCdTqckJCRYVwAwbv5pAWDOk8B7SIAESIAESMDOCFAAME8AEHct+2QVTp5Iw8DBEXjjleWIeflODBrSE8I94MjhFPz0/TqZXu/CcYNl1P/iogrEzr1bugd89/U6/LlmO26eej6WLlqLCVcOx3U3XojZT7+JM8nFuPnOkfj68y3oFRWGnr26QKTre2HenUhNTceN1zyD80ZGoWvXTsjJKQAULR585Gb8telfLH7vG9x21zj8sXon+vbvgdvuuAwHD5zAPzsPYc2qbSjIL8aU2y9G/4ERiI87giWLf8L0hyZj/75jOJ2ShZdencYYAHb2jLI7JNASCVAAsI0AYGoNoAoApkEAIyMjrSsA0AWgJT5+7DMJkAAJkAAJ1E2AAoD5AoBgt3vXIelPP3J0X2n6L4L/nUrOkFkA2rR1RVFRsYwR8M/Og4iM6o4BgyJkgxUVChIPH8e+vYkYMCgKoaFBUBThqV+EPbtO4PSZk+ga2g1RfULxy49bZWq+Sy4bjMLCQqSmZiInOw8ajTihF+6hWvSI6ApPT3cc2H8EiYeT0KlzEPr26y7/mpuTjyOJKXB1dYGzsxa5uQXoEdEF7dp54fTpDGz9KwHBIf4Yel5vIwye/PM1gwRIwDICFABsIwDUdAFgEEAz16l4Ay8tLYXBYJAmb+p/tTqtVmtmzbxNEBBcBUNhSij+i+8rPUhYSIAESIAEmpsABQDzBYCG565cXqJAkVkAAGf5nlhlWi9255rK90mNiLwvvldKoHVylUH7FFQAitiMi//ifpndCdBU1quBS8NdqPeKMuNf1fdkdeNPAcBCsLydBFo9AQoAthEAarMAUH8nXAASExOtkwZQnPxrNBoZBFDduDlSEMDszDxs+WubNKtzdRVvutWL1mDZG6FBSPqtuBicSqEvLkKfPn0wbPhgKbBUFpHdQgSVpMDSipcHh04CJNDMBCgA/DcBqt97cXExEhMT0bNnT3h5eTTzDLF5EiABEmh5BCgAWE8AqJkGMDQ01JCXl2fIysoyTf8n0wZa3QXAKD+ro3GYIID5/x+5d/26DTh16jRcXM4WAJxa9/7d4lccjbMGen0h+kRHYvjw4XByUk8ajGCrfra4KVZAAiRAAiTQRAIUACgANHHJ8HISIAESaJAABQDrCQDqCb+3t7chPz9fEQJAUlKSoWYQQJsIAI4aAyAvpxAb1m80CgBnm9M5SfM780trN3cXQYQLi/VGAeA8VAYVNlFVKACYv7h4JwmQAAlYSIACAAUAC5cQbycBEiCBswhQALC+ACDMpxuTBnDy5MnMAtDQM5mXk48NGzYhRVoA1CYAODdUBf9eDwGRH1lfXIio6N4Ydt5g496fAgAXDQmQAAnYAwEKABQA7GEdsg8kQAKORYACgG0EAHGKamoBEBwcbIiLi5OuAFaNAWASeaYqcpsaA0BRFB2AJABBLXXZ5uUWYeP6jUhJOQM3F7fKYRj+26BWWGYAAI1JXS2VkSX91mo1KCsvkQLA0KGDjFVRALCEKe8lARIgAWsRoABAAcBaa4n1kAAJkIBKgAKA7QQAYyA16fNv/N70q3WDAAohQHxQECbtjhQEMCe7ABvWbUDKyRRpAaBRg+xqZExelFXarJtdWrsAIIL9FZcUIbp/X4weM4IWAGavJN5IAiRAAtYnQAGAAoD1VxVrJAESaO0EKABYLgAIH38RiD89PV3d4MtNv2oBUJsAIIIARkVFWdcFQB2KMSOAIT4+XhcVFdWiLQD0+hLs3L4LmRlZ8PDwqExXJ4gaUwJWxqpnMZeAEIxEmsWuYaGIjKzMf8wYAObS5H0kQAIkYF0CFAAoAFh3RbE2EiABEgAoAFguABg3TGLzXyUANCYGgGjZGgnX1eSzsi7xYSE2NlYTExNjcAQXADGmkuIKlJeXV6UBFGMUeXnFV61lBgBAKw9yp6b902q1EO4AlVkA/3MBaO1BEvkmQQIkQALNSYACAAWA5lx/bJsESMAxCVAAsI0AYCoG2NQFoGYMAFMBwBEsABzzseOoSIAESIAESKBhAhQAame0b98+dOrUCYGB/vKCSvG6qcWsm5raiN1eT4HfbqeGHSMBmxPYsWMXAgIC0LlzJxQXl9i8PXtvwN29eqp5pbKkrV+//qYLL7wwvlOnTt4nT54sc3d3lyf+er1e9fFvXgsAo9m/8Y1QuCRoHMIFwN4XDPtHAiRAAiRAArYiQAGgdrLZ2dk4fvw4fHx8oNPpUFFR0fQpsDCNcNMbtLM7NHSitLMZYXdI4JwQyMnJkTHjevfufU7aawmNWEsAqGkBEB0dLbMAiLgAzs7OSmJionWDAAoBQAB2tCCAtl40rVv/t44Piq3niPWTAAmQQGslQAGg9pl3dnZGfn4+Tp48KV0Ey8rKIH7XpKJom3Q5LyYBEiABRyAghNOQjh0cYShWG4M5AoAIApiRkaEEBAQoaWlpQlFtMAig6mdtcQwA48m/2Mc6ZBpAq80sKyIBEiABEiCBFkaAAkDdE1YZu6Yyfg0LCZAACZBA4wiI95Xi4tLGXdxKrjJHAGgoCKBRIKiZDlCZPHmy5UEA6xMAGAOgEauWJgCNgMRLSIAESIAEmoMABQDbUTcYym1XOWsmARIgATsm4OTURIspOx6LNbpmrgDg7+8vUwGqFgCmWQBUASA4OFjR6XSGpKQkNV6A5QJAzSCARghCDmcMAGusCNZBAiRAAiRAAs1EgAJAM4FvFc0yBkCrmGYOkgRqJWBpGjXHwmquAGBiBVDlAmB0Bah58m8aNNByAaA2CwDj7xwmDaBjLTGOhgRIgARIgAQaR4ACQOM4mXUVgwCahY03kQAJkICjEbCmACAC/iUlJVUJAAMHDlR27dpVJQBERkZaTwBQgwCKCYmNjdXExMTQAsDRVifHQwIkQAIk0KoIUABoVdN9jgdLC4BzDJzNkYAdEaAFgOlkWFMAqM8CIDIyUklISLBcAFBdACgA2NEzxa6QAAmQAAmQgBUIUACwAkRWQQIkQAIkQAL1ELBEAPD09FQKCwulC4CIAVBQUGDIycmpsgAQMQBSUlKs6wJQWwyAmJgYWgBwmZMACZAACZBACydAAaCFTyC7TwIkQAIkYPcELBEAjHEAzooBEBERoRw8eNBQUwCwShaAmgKA+LCg0WgYBNDulxo7SAIkQAIkQAL1E6AAwBVCAiRAAiRAArYlYIkAUNMCIDk52RAeHm5ITEwUUf+rLAHCw8MV4++s5wJgFAIkHVoA2HaRsHYSIAESIAESOBcEKACcC8psgwRIgARIoDUTMEcA8PDwUIqKimRqPy8vL4Mw/Te1BlAtAEx+p1g9BgAFgNa8bDl2EiABEiABRyRAAcARZ5VjIgESIAESsCcClgoANQP/1WYBIIQAmwoARjGAWQDsaWWxLyRAAiRAAiTQRAIUAJoIjJeTAAmQAAmQQBMJmCMAGE/2pQVALZH/pTVAYGCgQavVKjqdTqQGlAJAVFSU9VwA1CwAwv2fLgBNnHVeTgIkQAIkQAJ2SIACgB1OCrtEAiRAAiTgUATMFQCM/v9SAAgJCTFoNBpFxAAw9f03dQEwfm8bAYAWAA61JjkYEiABEiCBVkqAAkArnXgOmwRIgARI4JwRMEcAMI0BUJcFQG1BAK2aBUBYAFQG/2cQwHO2WtgQCZAACZAACdiQAAWA2uE6OTnJPwg+NUttvzNvisQhTn2lsg/NVwyA4lzZvKYcQHP3p/lIsGUSIAESsISAOQJATReA2iwAhAtAamqqaTYAxeoCgDpwNQ2goig6AEkAgiyBYpf3nv2eb5fdZKdIgARIgARaCIFKDd2uCgWA5pkOg8EAo8ZQTwe44W6e2WGrJEACJGBdAuYIAHVZAPj4+BhycnKqMgIEBwcrKSkp6s+2EwBiY2M1MTExBkcQAEpKSqTCr/6XQjc3/9Zd9ayNBEiABEgATs5auLq62hUJCgC1T0fl5tzJYgsAdb7FZw3TooEWFYYSaLVaODs7ya+qlaUQB8T/sjLLPoyIek2L6eccRdHUOrbqNGgBYFcPKztDAiTQYgk0VQDQaDQGkQLQNAaAMPfv2LFjnTEAwsPDlcTEROsKAGoaQPEGogoA8fHxuqioqBZtAVBaYsC2bTuRkZ4FNze3s4ImiDdhFhIgARIgARKwlEDX8CD06tXL0mqsej8FgNpxpqdlQkEFQkJCoNfrkZeXh4qKCgQGBsobxGZddQWo6RKg/iw2/4cPnkBubi769O1RrSFDhQZO2goIV4Mjick4fuyMrK+0tBw+7bzQf2AEdDphZNn0ItqNj4+HUqGDs7OzrLNP37BqBx2irYY+3ri5uqKktLQRQkHT+8g7SIAESKA1EWiqAKDX66tO9OvLAiBEgZoWAPI9ylK4Rt9/RY0BIN40HMkFIDenEOvXb8CpU6fh4uxaDZiTAqnCs5AACZAACZCApQT6DYnEoEGDLK3GqvdTADgbp/igtmnDP7j39lex4pdXEdErGDu2xyEnuwCXjB+J9LQ8aJwMcHd3R3ZWHsrKKtCpcwDKyw1Vm2zx2aG4sBS//7Yd4nPGtIevQnZmIbKyctCunTc8vTxQUVEGDy9XxD79EY4dOYMBg3ugpLgMQcHtccnlwyD64erqguzsfPm9m5sOaWlZssMBAb7SQkG0r9eXwD/ARwoGBQVFKC0pw5+/78DplExcNXkMiotL0bNXF6ScSpdigJ9/W1mXKGmp2XBzd4W3tzu+/2YjXHTOuPLqUchIz0VWTq6s18NDB8Wgg5PWMosEqy5cVkYCJEACLYiAtQSA+iwATGIGWC4AGEUEKQBIRaFS9RYagMERLADEG/OGDRsrBQAXlyrzf7H5ZyEBEiABEiABaxEYMCwaffv2tVZ1VqmHAkB1jIKH2Bzv2JaA6XfNQ6dOHfHFd8/iwP5EZGbmwMXZAyuWb8LoC3ojpGMgVv74lzwo6BUVimtvuFCe6JeVleH9d75Dfo4ex44mI7pfOG689SIs+/h3FBeXoWMnf0ycNAZt2rrD3VOHF2YvRbfwjrjxlnEoKSmDi4sW++KPIm53Ikaf3w/LP/8Dl04Yhh9XbIRve2/s+ecw7n/kWnh6uuOrZX8gL68AQcF+uOOeiXhv/rfIycmHYtAgNCwQAwb1RHFxCdr5emHNqh3IysxFvwE9ENGrM1b+uBVubq7IysrF1ZPPx0eLfpZtP/rEDfjmyz+RnVWAbj0Ccc31Y+Dq4gONc4VV1hwrIQESIIHWRsAcAUCNAWB0A6hK/afGAIiIiFAOHjxYFQvAZgJAzSwAjhADQKjlGzdsxunTZ6R6rvr/M/ROa3s0OV4SIAESsC2BHlHhtACwLWKLaldN9z083LBh3U4cOXwKWZmFKCkpxvU3j0Z+nh4/rvgbThod7ntoAubGfoa77rsSHTv744pxT+DL755HcIg/dm7fj4Vvr8DCD5/EogUroKAEAR3aIunoadxw8yV4OeYLXDZhBK65fizgpGDOUx8hP68Ql185QrYR2jUI4T1CsPzzNdi96xAmXTsaET07465bX8bn38Ri84Y9+PuvvfJ0XnyovPzK4bjxqufw0OPXY+f2BMx9Yxo+eO9HKUxE9emKkyfSEB+XiJFj+kmh4I/V2xHWLQS//rQNn30dg+dmfYDOXQLh7e2JkE5+0mqhtKQcd99/Cea98A1GjOqNqfdeCL3exSK+vJkESIAEWisBcwSAmlkATFP+mX5v6gIQGRmpREVFWdcCwBHTAApzuNycfOnjJ4PwGFcmLQBa6yPKcZMACZCAbQho3Zzh79/eNpWbWSstAP4DpwbJEyfr69fuwPa/D2D6Q5MR88wi6IsL5GZ/5Y/b0blLR0y+cTTum/oaXp1/PwI7tMcV42bijXcfRPcenbF2zU5s+3sfZj17G1b+tBG5udnIz89HWx8v9O7THVs27kfvPr0wYHA43DxcpAtA17BgaQGQm1soN/We3m744ZsNiJ29BN/+/BK823jh4w9XYuYzN0lRYNGC7+HqpsWosX0R1TtMmvz7BbRFemoOHp5xPVZ8vR4pp9LQb0B3xO05gt27DuKKq0YhOMRPCgJirBlphbjn/qvx0pyl6BDUXv4PDQuS1gsdOwXg6uvGYM2qv9GjZ2dcPH4o9HpaAJj5mPE2EiCBVk7AGgKASAN46tQpNeVfzZN/62YBUGMA1IgnIPbJDuECAKg+/jzzb+XPJodPAiRAAq2OAAWAugWAdWv24JnYqThz5gwuufgqLFm6CL/9uA/tfN3w2FNXI/bZRRgydLA0xX96xkL8sPpl+Pn54vChY7jn5sV4e9Fd0qy+TRsPDB8Vjb3/HpZuAj99txGDh0Vi6Hm94KLT4ukZi6DVanDZxBHIzy9Cmzae8PB0w6pftsoNuwgSeM31F+D+u17H4o+fwi8/bEabth4I7OCLjPRsjBrbHx++/wMuv2IEvl2+Hg88eo10DfBu44bI3l2RcioDmRl5iOoTioDAdjiQcBz+Ae0QtzsJM5+eIi0AOnXuAE9PV3h4uiP1TBbS07KktYJoq9/A7hhz/kDUzGRQ6RjKUheBmsEhzzUp9dDuXLdrL+219vE39zw09/pv7vHXbN9cAaBmFoCaAQEDAwMNqampqiggvkondmu8PIs6RGWmdUkBwBFcAOxtgbA/JEACJEACJHCuCFAAqE5a8BB+/MeOHkdmZib69+8v4wMdPHhQRtTPTC+Bp7eC/v374MiRZGmmf/jQEcycdTei+nRBfkE2vL3a4e8tO/Hdl1sxYFB3RPYOQ1h4CFZ8tRbbt+7HJZcNxQUXD4KzM+Du4SpP7zeu24OKCgXl5eUyoGBEr1Bp4t93QHf89tNWuLq5SEFACAMajYLHnrxJmvh//NFKxMcdwdXXjcX4icPx289bsPLHvzF8VG/06RsurxX99mnnjQ8X/igDA069ewICAn2QfDIdI0b3w4a1/6BtWy/Z9t+b9+HWO8dLAUHEGhhzfj9MvGq0jA0gsiCYFgoA5+opZTskQAItnYAlAoDYhxcWFlbFABB7cF9fX0NWVpbpxp8WAC19kbD/JEACJEACJHAuCFAAOJuyKgK4uupQUlJadYHYSKtFnIaLuEGVG2ORUs8gT8iFeCDcCcVXF62TzAwg/iZOI52dnaBxcoLBuNEXFojCDVEEHXTSAk5aLaAoItqybEapgIzgLwIFppzMwNYt+zBx0gi5oRfBAoXVgLNLZZ3ivvKyyiwEoi6t6Ksi2qqQG/fK9p1l3YrBIDf7IouA6JvoqxAKRNIjMR5Rt8g+IO5RRYnaTvMoAJyLJ9T8Npr7BJYn8ObPHe90PALmCgDircA0CGBjsgBERkbaxgIgJiZGExMT4yAuAI63yDgiEiABEiABEmgMAQoAtVNqzOapoTTBGqV+18LK/bhGbrhFe2JDLxIumX5f3xw22EdFW+8S0DpDbv7VcYjvxX9VyCgpLoerMV2gEC9qjtfeBYDWvgFtcH005gXCgmtaO38L0DnErc29/uwNojkCgJoFoKbZvzEAoMHPz0/JyMgw1AwCmJCQYH0BQExobGwsBQB7W1nsDwmQAAmQAAk0kQAFgCYCa8LlDQkA4tRdbJJE2kBRhOWAKOLEXv2+Cc3Z7aX2LhTYChw3QLYiy3pbAgEKQNVnyZoCgJoGUAgDtcUAmDx5sm0EAE3lrNICoCU8gewjCZAACZAACdRBgAJA3UtDsBFm8+JUvLS0VMYCaEppSAD4LwhxU2p1nGttLQy09g04N2CO86y0xJFUuhmxqATMEQDUNICmLgDid6YCQI3UgNYPAiiyAaiDoAUAFzQJkAAJkAAJtHwCFADqnkNxMi9SBIvNvyg1N5QNbbAoAJj3fFhLGGhofszrHe8iARJoDIH27e0r5W1j+mzLa8wRABpyAYiIiFBycnKqZQGIjIxU6AJgy5lk3SRAAiRAAiTQwglQAKg+gcL8XgT3y87OxokTJ6SJvqurqwyaV9Msv6ETZgoAzftwNDQ/zds727fe3OOnAGP7ObbnFjRaJ3Tv3t2eu3hO+2aOAKBaANSMAVCfBYAQAKKioqzvAiBoMQjgOV0zbIwESIAESIAEbEKAAsB/WFWTf3Hif+DAAfj5+SEkJKQysr6ItN/kIrIysTQXgda+AaUA0Fwrj+0KAv/8swcBAQHyPwtgrgBgNP9X0/3JVH8NuQBYJQaAMP3XaDSK8aucQwoAXMokQAIkQAIk0PIJUACoLgCIn4QAkJiYiF69esHT011eILLzNb2YdVPTm7HTO1r7Bry5p4UCQHPPQOtu/+jRJBQUFMjXUcYDME8AaMgFwMQyQAoE4eHhSmJiomL1NIDixUS8oKsCgKIoOgBJAIJa9zLn6EmABEiABEig5RGgAEABwFarlgKArciyXhKwfwIUAKrPkSUWAGKjX1hYKE7/pQVAzTSAAwcOVHbt2iX/ZrMYAGI4tACw/wePPSQBEiABEiCBhghQAKAA0NAaMffvzS0A8ATc3JnjfSRgOQEKAJYJABqNxlBUVKRG9a/mAqAKADUtANSYAVZxAQBkHAHRcFUWAOP3DpYGsC5fPXP8/ix/cFgDCZAACZAACdiaAAUACgC2WmPNLQDYalyslwRIoGECFAAsEwD0er162q+KAGdZAIj9eWBgoMwCEBoaakhKSpIWADYLAqgKAI7gAiAi/tYsGhO3PUVDAaDhx5xXkAAJkAAJNERAqzXV0Ru6+tz8nQJAdQFABPsrLi6WMQB69uwJLy+PczMRbIUESIAEHIgABQDrCAC1BQGszwLA6i4AIgigGIppDID4+HhdVFSUY8UAqBmzx/4+rznQywOHQgIkQAIk0JwEKABQAGjO9WfLtpvbBcCWY2PdJGDvBI4dO84ggCaTZEkMgMLCwioXgI4dOyoFBQWGnJycqngAwcHBSkpKigwCqNPplISEBMvTAKpZABzVBaC4uBTHjh1DXm6BzP1rut93UoAKCgD2/hrD/pEACZBAiyAQ0jHQ7lIiUQCgANAiHh52kgRIoEURoAWA9SwA6goCWFcMAHlgb+lqMQYuHrwAACAASURBVBUAHDELQE5OHtav24Tk5JRKAcBoASA2/ywkQAIkQAIkYC0CUf0jMGzYMGtVZ5V6KABYIgDUFTtIrbMhF8L67zcaXtYzz5X3/+dr31B7Nauq3n7N9ujDb5VHjJWQQKskQAHAcgFATQNodAMwhISEGDQajZKcnCzjAfj5+SkZGRmmmQFkvACrBgGszQXAEWIAFBaU4I8/1iHl1Bm4uXlQAGiVL1McNAmQAAnYnkDUgG4YOHCg7RtqQgsUAFq+AKCKABUV1U8utFptAyuBAkATHhVeSgIk0AQCFAAsFwDUqP6mAsCpU6eq3AEiIiIU4QogggAKQUC4ACQmJlpXABAis+pPFRsbq4mJiXGILAAF+cVYtWo1Tp06DZ2LG6BUBgXUGL82Ya3zUhIgARIgARKok8Cg8wZg0KBBdkWIAoBtBQCDwQDVelIEGFQUcWKvtmkdCwBVADh7YTVkEUABwK4eRnaGBByIAAUAywWAmhYAxuB/taUErBIArB4DwOgKoL6RibcvhxAA9Ho9Dh48DH1RiXxjhkGRY3RSGnrjdKCnlEMhARIgARKwOYGgLv4IDQ21eTtNaYACgPkCgMFQjh3bDiA9LQeDh/ZCQKAPDAaxwVew99+jOHbsBEaPHgFfP3eUlxvg7OyExIMZOHDgAEaM7oO2bb3hZPyoceJ4qrynd3RXdAntYKynbi/OfXuP4tjRFPTtH45OnQOkG4CCCiSfyEZ83GH0igpDaNegqsGdSMrEzp07MXhoJDp16iR/L8SJ7Kx8bNkch8jeXRHWLaTa0qELQFOeJF5LAiRgSoACgOUCQE0LAFUA8PHxqTUIoHq9FIWtsBxFHWJrbFqXwwgA4g1QvMlVvdHR998KS4ZVkAAJkAAJnEXAGu/IVsZKAcBMAeD/PyvEzF6MzRvicPH4wVjx1TosWPw4hv5fe2cCF1XVv/FnhkVWJUQU0EQjF0ZxQ81Wk9Q0s7eFFn3LLN/W18p8K5cy0BZbbbEss8xcWm0vs9zN1Mwlwi1RUQEVUQEZdub+/+cyly4jqMzcgTuX534+Iwj3nuV7zhnmPOe39IvFKy8sQsq2vYhu3xIH9h/BPQ9cj74Xx2LqU+/jt7WpuGpwPL7+Yg0mPDkG1914MaZPW4B1a7fg8gFd8fVnf+C2kdfiwXFDhSlitdEWaYu9vb0x48VPsG7tn7LosG7NX7j2+ktw973DMXvWF/ho7he44aZrsGjeSvx7dALGPX4rPv94Od6b9S0uv7I7tm3eg0uv6IZxj92Kjz5YijdnfIabbknAht9SceVVvfDQo4nyIYj8AdJ0NhcCjScjiyMBEjAMAQoArgsAigWAKtif7O/vKAAov9c0C4B9418lANhN2QwjAJy20pSeUggwzJsQO0ICJEACuiBAAUAXw1BbI2TrP7MZxcXFSEtLQ6dOnRAUFFDj7dtT0/CfO57H4h+eQ0REc3z1xWr5JH3U3cPwxiuf4vHJt6N9TATef/c7HDqQjVtGJuDW65/Cit9monlYM3z52SrMfnM13l/0X0wY/xqemnofOnVuhx+/+w0fz1+KN2b/FyHNwqrVLQ4qjufk4bLe9+LdD5/AZVd0wx+/78a893/EQ+MTMXzgBCxYPAm94jshZdsB3HPnM/hxxSu4Z9R0jLhjEG5I7I8/Nu3EvDk/4s7/CJFgOQYM7Ilrhl+CHdv3Y+w9M/Dxl1PRIrwZBQBdz1Q2jgT0T4ACgHYCgBIDQFgAiDSAIghgaGio7cSJE4o7QNXX2NhYyWKxaGcBYFQXAAoA+n8TYQtJgARIwBAEKADoehjrIgD88fsuPPnEW/j+l1dk0/6DB7Jx18hn8Z8HhmPf3kzcP/YGBDf1hzDV/3rxGkREhskb7zWbZsFsMmHrlt14/KG5eOCRofh17SYkPfMggoOCsen3nXhp+ruYOfsxtAiLrMZLWAAczsrBbTdMkX/frUd72bXgpmsnYuy4mzFx4lP49ocP0aJFS5SWFmLkTU/j33dejT1/Z+DGm/vjwg5tYLUW4elJ78nuARkHj+ORx25Fq4hQ5BzLx9Sn5siWBMKtoPKwhxYAup6wbBwJ6JgABQDXBQBxsi82/2dLAxgZGSllZWUp2QDkI2wtPm7ILgA1ZQFITU31tVgs6QD+cTTT8WSsU9McnR7q9DBvJgESIAESIAH9E6ALwD9jVBcBIDNDbMQn4q57r5XN/sePfVP2p1+0OBmPPTwTlq7tMHLUIEx96gNkZR7D67PG4fFHZuL6xP645LI4fDz/F3z6yZf48Zf3MXH82+jWMxojbh+OV6d/hk2bNmPFb28hMKDyJF65FFfFaxIeRfeeF+K22wdh1YotmDh+FpavexMfzf0agUE+uHvMHfh40deY9cbX+OrH6fhl6UacPJGPex+8AQs+/AnffLkG7y+YhJ++3yS7KkydPgbLlv4huxYs/CIJF1/alQKA/pcuW0gCuiZAAUAbAUDl1y+n/lMsABwCAqotAaTY2FjtBAAlC4D9D5DsAmCENIC6Xj1sHAmQAAmQAAm4kQAFAOcEAPHUht/+ks3vK8olXD3sIrz8/AJ88d3zOHXKipkzFkOSKtCjVwccPXISd465Rvbff37qPBQVlchB91Yu24xPvpqGv3cdkn30RTDiXn06YPtf6Rg/4Va0ja5uAaC09Mjh43jhmQUoshbjyoSeePHZhbJLQPuYSDw98T3knjyFIcP6YfFnKzE5aTQ6dj4fLz03H7t3HpTN/f/cmoZ/3XQ54vvE4sVnP8K+tMPoe7EF+w7swrXDh+Dy/j3dOONYNAmQQGMgQAFAGwHAbv5/WuR/tQuAWywA7Kb/sgWAoj4nJSUZJg1gY1iE7CMJkAAJkAAJ1ESAAoBzAkB2djZmvfENxo67BaHNg7Dljz2Y8843eO7l+/D6y59ixB2DEXNhFL77eh02bdyOiVNGYfq0jzDq7qG4IKY1Vi7fjBkvfIZvf34BUybMwc0jEuQMAN9+9SsWfLgUs+c9IWcJcLyKi0vlE39hXdC7TywyDx3Fm69+gfvGXi/HD7jo4i7o2bsTMg4ewS3/ehJrN72LF5+djwED4xHfpzN27kjHuzO/xmOTRmDtqj/RtFkghl1/CbKPnMQzSe9j8tP3IbxVcGU2JCVFAZcOCZAACdSRAAUAbQQAZywARM0uuwAoAoDaAoACQB1XAW8nARIgARIgAR0SoADgnABQUlKCqU9+gPz8QnSKbYsN61Jxx11DMPDq3nj4/teRvu8wbrj5ciz/+Q8MvqYvRt4xGNOmzMWO1P3yZnzblj247obLcNXgPnjy8XfkU/mbRwyQ4wWIU/rR/xlW6wY8adIcbN60Czcm9pdTB17Wv5tc1vy5P2H5z5vQ79Ku2PjbdrSKPA/TZzyIt2YsxqrlWzB4aF/Z6qBLXHtMTBqFzxYux+y3vsGNt1yJjENH0cTfF1Om3QWAaZB1uFTZJBLwKAIUALQRANQWAMHBwbZmzZpJBQUF1dIAKlkAhgwZIi1ZssQ9LgCiO8nJybIFAF0APGotsrEkQAIkQAIkUI0ABQDnBADxlPD5F/711oJinN+2JRIG9YIkmSBO6Rd9tFQODtixc1v06t0JPj5eyM0twLKlv8sm+ue3jUDCoN7w8jLBai3B0h834uiRHDk4nzDfLygoxqn8wtNma8h5wfDz85XLF1kCzwttKqfv8/NvgtKSMtkK4HhOLpo2DcKNt/SHWcTxM5nw3Ze/IvvoSTkY4RUDeiAw2E8ue9Wyrdiz+yBatgrF4GF90KRJEwoAfI8gARJwmQAFAG0EACUQoNVqlWMAKGkAzWZzjVkA7GKA6xYA6jSA/+SGNRk3DaDLU54FkAAJkAAJkIBnEKAA4LwA4M4RXrFiBVb/sve0Kob96xLZlN89lw2Vrp60AHAPX5ZKAo2HAAUA9wgA5xIEUNSsmQuAOguAvVybobMANJ41yp6SAAmQAAk0UgIUAPQpAIhWKYcu9Tc1KQDUH2vWRALGJkABQDsBQDHxd4j8r6T9c/wqJSYmui4A1GQBQBcAYy9a9o4ESIAESKBxEKAAoE8BoP43/4IDBYDGserZSxJwPwEKAO4RAIQFgBIDoGPHjlJubq7t6NGj1dIAamIBoBYAFEWaAoD7Fw5rIAESIAESIAF3E6AAoE8BwGZTNuPungHq8ikA1Cdt1kUCRiZAAcB1ASAgIEAqLCwUm/uqNIBqAUD5uWMaQLdbANAFwMhLl30jARIgARLQkoDYbCvpdLUs15WyKADoUwBwZUz5LAmQAAk0NAEKAO4RANRigLAA2L17d/27AFAAaOjlxfpJgARIgAQ8hUB5eTm8vb111VwKABQAdDUh2RgSIAFDEKAA4JoAYDKZbKrT/zNaAAhRIDo62paenq5YC7gnBoCp8giDaQANsUTZCRIgARIggcZKgAIABYDGOvfZbxIgAfcRoADgmgBQVFSknOxXcwFQLABCQ0Or0gA6ugCIml3OAuAYA0AUmpSUZEpKSjJEFgBxIlNcXAzlZMZsNsvfi5f4/twucZ8Yp3O9/9xK5V0kQAIkQALGISByvgcHB+uqQxQAKADoakKyMSRAAoYgQAHAdQFAxAAwmUyS1WqtZgGQkZEhNp22sLAwKScn5zQXAM0FAMV/0UgCQElJGX75eTlyco4jMDAYZpgggu+Iq+YovNzkG+KdiZ0gARIggXom0MkSjbi4uHqu9czVUQCgAKCrCcnGkAAJGILA/v0HkJeXB4vFIh+qNvbL379JNQRS5ZW9atWqEQkJCalt2rQJPnToUJm/v7984n8mC4CQkBCbiP6vCAButwBQbYiFF4AhLAAKCgrx89LlOHo4B76+vpBsJsBkq3nzL53r5l8Lw4vGvlTYfxIgARIwFoGefTqiV69euuoUBYCah2P79u1o06YNWrZsYT8QqPuwyc6SvEiABEigERLYuvVPBAQEoEOHC1FcXNIICVTvsjMCQE1ZANRBAB0tAGJiYqS0tDQhINAF4GwzLjc3H8t/Xoljx07KJv8ivIGXjM0mCwFV1xk3/+cqDJytNfw9CZAACZCAUQnExXdCfHy8rrpHAaDm4Th58iTS09PRvHlz+UOsU9c5Hxo4VTofIgESIAHdEJBQAS8vL9mKOicnByUlJYiNjYWPj49u2tiQDamrAKAOAhgYGCjcAGSzf3UaQJEFQFgCHD16VHERcE8QQAFOfFhITk42TAwA4QKwbs16nDyZJ/vwm2wVMJmr+KnmSi1SPv/AN+R6Yt0kQAIk4DEEOsXFoHPnzrpqLwWAmodDZGvIysrCkSNH5MwNzqRvtFXQBEBXk52NIQEScBsBsfEXm/2Kigr5PfP8tlEICgqS/88LqKsAUJsLgBAA6j0GgBEFADExxQcgMXHFV+XFyUoCJEACJEACWhPw9/fXukiXyqMA4BI+PkwCJEACJEACZyXgjADg6AIQFRVly8zMrAoI6BgDQHEBSExM1M4FQJIkWcoWKriRggCedcR4AwmQAAmQAAkYlAAFAIMOLLtFAiRAAiSgGwLOCAB2f/9qaQAdLQC8vLxOcwEQrhda2J+JMkTlcllGcwHQzcxgQ0iABEiABEignglQAKhn4KyOBEiABEig0RGoqwCgxACw+/9XnfqLrbiSBUAVEFD+/ZAhQ6QlS5ZImlgAiJN/kYNQsQCwWwHIUfJSU1N9LRZLOoAITx5JJb2hJ/eBbScBEiABEiCBuhKgAFBXYryfBEiABEiABOpGoK4CgDoGgDoIoCIAmM1m24kTJ9TB/0SQQNlaQBMBwH7yX80CQCgCRhIA6jaEvJsESIAESIAEjEGAAoAxxpG9IAESIAES0C8BVwQAdeq/MwkAbkkDqLYAMFIWAP1OFbaMBEiABEiABNxLgAKAe/mydBIgARIgARLQUgCozFUPmxIEUBEI3BIEUIkBYB9CWgBwLpMACZAACZCAhxOgAODhA8jmkwAJkAAJ6J6AVgKAOgigYwwAt7oAUADQ/RxjA0mABEiABEjgnAhQADgnTLyJBEiABEiABJwm4KwAIPz/xcbearXaHNMA1iYAiEYyC4DTQ8UHSYAESIAESMDYBCgAGHt82TsSIAESIIGGJ+CsAKBKBSib/TvGAxA/i4yMlLKystwXBFB8UJBVBQYBbPiZxBaQAAmQAAmQgIsEKAC4CJCPkwAJkAAJkMBZCDgrADimAazJBcBRAJD36hqMiChDzgKgCAAMAqgBVRZBAiRAAiRAAg1MgAJAAw8AqycBEiABEjA8AWcEgICAAKmwsFCqKQ1gbm6u2hpATgcoggD6+vpKFotFOwFAZAGoPPgHkpKSTElJSbbU1FRfi8WSDiDC8CPHDpIACZAACZCAwQhQADDYgLI7JEACJEACuiPgjAAgDuDVMQBqcwFwiAUgm+tragEgChQfFmgBoLt5xQaRAAmQAAmQQJ0JUACoMzI+QAIkQAIkQAJ1IuCsAFBTDICQkBCbsABQpwGMjo62paenywEDNREA7Cf/kvgqF1hpBcA0gHUadt5MAiRAAiRAAvojQAFAf2PCFpEACZAACRiLgJYCgN0SoMoFQB0DIDY2VtqxY4f2FgBiOOyiAF0AjDU32RsSIAESIIFGRoACQCMbcHaXBEiABEig3gloJQA4BgGMi4uzpaSkSJpbANhP+2ULAHH6r3YBkCTJFwBjANT7NGKFJEACJEACJOA6AQoAtTP09RUfcSpdH202m/xV/XKVvhJXydVy+DwJkAAJkIC+CTgjAChBABUf/6ioKJvJZJIyMjLklIBqFwBVfABtXAAUAcCoWQBKS2z4/fdNOH78JPz8fGEWRhOmynSHvDyfgCSWCC8SIAES0AGBtu2j0LFjRx205J8mUACoeThsFcDxYydRXm6TN/8BAX4oKyuDzQa0bBUKb2+z/PO6XBWSTXGjlB+jAFAXeryXBEiABDyXgDMCgMr/X47yL141pQGslyCAAr2RsgDk5VqxcuUqZGRkwce7iZD6q2aXyUYhwHOXWmXLTfDy9C6w/SRAAgYh0LNfLHr16qWr3lAAOH04xAe1tau3YOqkuWgfE4Vj2ScwZFg/dOtxIcrKynHJFXGQKoCCgkIEN/VHUWEZfHy84O3rhZKiUruVgAminPz8QgQEVFoSlFVUWhEoFwUAXS0FNoYESIAE3EZASwGgoKBADgLosPFX/i8lJiZqFwNAnQbQSEEAhQCwZs1aZGUdgY+PT6UFAAAvTdC5bR6x4HMkoP6wdY6P8DYSIAEScAuBrr06o0ePHm4p29lCKQBUJyd4CGvAX9dsxZ+b0zD20ZsqbzCZkLI1Tf6ckLJtD/buycSVV/XCseyTOHkiH3l5VoS3PA/WgmI0D2uKhEG98eGcHxAeHor8/AIMGBiPNtERKC8vpwDg7GTlcyRAAiTgoQTqKgCYTCZbYWGhEtW/ygJAtek/TQCIiYmR0tLStHEBUGcBUNRqQ1kA5J3C8mWrcOjgYXh5eQE2RQAwe+gUY7PVBCqTVvAiARIggYYnEH9JF8THxzd8Q1QtoADwDwxFMBbm/sICYNbri3F5/24QSZAu698Nmzftkj8nbPxtO3z+/7R/4pRReOu1L9D34i7IOHgU+/Zm4qZbEzB39vf477gb8cfGv3F+dAS++nw1hg6/CFcO7IPS0lIKALpaAWwMCZAACbifQF0FgKKioqoT/Vo2/TVaAGieBUAdBLDSdc1kM0IQwNLScuSezEdhYWGlAGA3zzNLlYF/eHk2AY6hZ48fW08CRiLQtHkwmjVrpqsuUQCoLgAIHoGB/li3dhs+nf8Lbrt9EExmCdHtIrBx/Xb55tQ/96PfpV1wyeVdsfjT1Rh67UXY/lc69qZlYOSowZgyYTauvqYfThy3yu4Da1Zuw+h7huGyK3tSANDV7GdjSIAESKB+CGgpAISEhJzmAqB5FgDFAsBu9i9viuV0AIBB0gAqQXx44l8/S4C1kAAJkEDjJGD/+6mrzlMAqFkAWLn8d2z8dQcen3y7HOxP+PcvnLsUPr7e+HPrHvTtZ5HjAiz4cCmuvuYibNuyB+n7D+P20Vfjmac/QOs2LbFhXSqmvXAvkie/j8FD++LG2xLoAqCr2c/GkAAJkED9EHBGAHDMAqCK9C8HBKwtBoDokRYG0KIMcRReVZaRXADqZ9hZCwmQAAmQQGMnQAFA/zNAjJHZbEbann3Yu/uIvHEX1oHiZxt+S4WXlxmHs06gzfnh6NqtPZb/vBnxfTsh51ge0vcdxoCBPfH5xyvQKTYay5Zugr+/H/LyCtAqojnuGDOUQQD1PwXYQhIgARLQnIAzAoCSBSAwMFCyWq21ZgGIjIyUsrKyZEFAcxcACgCazwUWSAIkQAIkQAINSoAWAKfjV4QaVzMCi1SBIgtAaPNmKDhlhcnLzDSADTrbWTkJkAAJNAwBZwSAulgAqF0ANM0CIAQA5Y8iLQAaZvKwVhIgARIgARLQkgAFgJpp1kf8GKYB1HImsywSIAES0C8BZwQAxQKgtiCAYWFhUmRkpC0lJUXSPAaA/eRfdgGgAKDficWWkQAJkAAJkEBdCVAAqCuxf+53VSSgAOA8ez5JAiRAAp5EQAsBICoqypaZmVlTSkDlZ0raQPfGADBCFgBPmjxsKwmQAAmQAAloSYACQO00BRtvb2/Z/1+k7/Px8al2swgOyIsESIAESOB0Anx/rM7EGQFAcQGoKQZAaGiozWw2Szk5ObLvv7AA8Pb2ltLS0uQUdi4HAawpC0BycrIpKSnJIFkAuGxJgARIgARIoHESoABQ+7iLjX9RURHKyspkEUBc4qtycl9RUdE4Jw17TQIkQAJnIRAQEEBGKgLOCABncwGoKQuA5kEA7UKA3BXGAOCcJgESIAESIAHPJ0ABoPoYik29r68vCgsLcTj9EKxWa9UNwgJA/F4wE5kBeMLl+fOfPSABEnAPgeaRLREeHu6ewj2wVC0FgJCQEFtubm5VGkB1FgC7KKCdBQAFAA+cbWwyCZAACZAACZyBAAWAf+AoJv/C3H///v3yBr9Dhw6yICB+Jzb/YuMvLAB4+s9lRQIkQAI1C6gnT57Erl27EBMTg2bNmhETgPoQAGJiYtznAmAfReFaQBcATmkSIAESIAES8GACFACqCwDif0IASEtLQ+fOnREY6C/fIMleldUvk8tOlh48cdh0EiABEjgDgX370lFQUCC/j5aXlzd6Vs4IAGdKAyhiAJw4caIq+J+SBUC4AFgsFtctAJQsAIoFgD0TAAWARj+VCYAESIAESMDTCVAAoADg6XOY7ScBEtAfAQoA1cfEGQFAaM/2AIA1Rf6vcgFQxwLQPAYA0wDqb3GxRSRAAiRAAiTgCgEKABQAXJk/fJYESIAEaiJAAUAbAUAJBBgUFGQrKChQb/ptYWFhVVkAhGW+KmigdhYAagHAbhVAFwCueRIgARIgARLwYAIUACgAePD0ZdNJgAR0SoACgLYCgH2DXyUAOLoAiN8PGTJEWrJkiZSYmKidAKAOAqgIAJIk+QJIBxCh0/nHZpEACZAACZAACdRCgAJAdQFApPkrLi6WYwB06tQJQUFMZcXFQwIkQAJ1JUABwHUBwDEGQFRUlM1kMkkZGRlCCJDFgLi4OFtKSkpVLADNXADsG39RcFW4G6YBrOsy4P0kQAIkQAIkoD8CFAAoAOhvVrJFJEACnk6AAoBrAoDJZLIVFhaK/bfyqtr0O6YBdIwBoGkQwJpcAIxgASCU/vz8fBQVFcnpfYT6r+T29fb2ZqofD3gHEimZxJgFBgbivPPO84AWs4kkQAIkoA8CFAAoAOhjJrIVJEACRiJAAcA1AaCoqKjKp98eCFAWAFq3bi2JWAC5ubk1BgEUYoCmLgCKBYDRsgBYrUX44fslyMo6jCZN/AGbVCUAiL4KUYCXfgmIzb+4RIqRFi1a4JYR1+u3sWwZCZAACeiMAAUACgA6m5JsDgmQgAEIUADQTgBQTviFC0BmZmZNGQGqXADEvbGxsa7HAFBcAGqKAZCamuprsVg8OgZAQUEhfvl5BbKPHIevry8gmQGTEFV4eQIBIQCIl8jb3Lp1awwZdqUnNJttJAESIAFdEKAAQAFAFxORjSABEjAUAQoA2ggAjmkA1RYAShaAyMhIKSsrS7YI0CwGgP3kX44BYD/9h10MMEQWgJMn87D855XIzj4hbyTFyywBJpP4h0KA3t+N1AJAdHQ0hgwfrPcms30kQAIkoBsCFAAoAOhmMrIhJEAChiFAAUAbAUBs6oUIIGICKGkA1TEA1EEAY2JipLS0NLFn19YCQC6w0uRa/GMzRgyAUqz/dSOEEGAyecFkq5A3/qKbinm5YVajATtSUVEhx20QFgAtW7bE5QmXGbCX7BIJkAAJuIcABQDnBYCiIiv8/L0AqQlMpnIAPigqtMHfzwwbxOcIc42DZvdcE85rALwrQzzJn6zEoUMFSksr8OeWQ+gU2wZ+fk3g7SMOJ2oq6syHFBIqKq0aAZSUlMFs9obZ5A1vVZWVn+tqnluS3C7FshTYtzcTPj7eaBbij79S0tCnbw8cOnQI/v7+iIxshbKyyr/HIjj1Xyl/I7xFK0S2rozLI3+0stdTCxb3THCWSgIk0CAEKAC4LgAoWQDUMQBUb8q2jh07Sk2aNKmWBcBtMQCUP1Pi/dwILgAieJxyitwgK4SVkgAJkAAJkEADEaAA4JwAUFBQgIfvfwlXDuyOkbdfD8kGiL3vrDe/RmjzENwysr89S9OZBlZs4L3tN9hgk8rlDXpmRg7uHf08Zswcj5gOUfJnFBHnRsQkEt+LjbaXlxAFKpWD2g4rlM83BQXFeODuF3Hk8HH8snam/IwYd/VVUxmSVAFAETFseOWFRejarR1atGiOm66diK07F2LlypWIjIxE7949YTKXQ5K8cSq/EP8ZPRmPPDoG/S612NtbGU9JLTbUJjw00FJgtSRAAhoSoADgugCgygBQze//bFkAZCgLhwAAGlJJREFUduzY4boFgOICoMQAEH80kpOTTUlJSYYQADSc6yyKBEiABEiABDyKAAUA5wSAwsJCXHf1OGzZvB0//PIOLurXBTYb8Ezyu/LJ931jr4PVasXG9duRcywX8X06o/0FESgqKkNJSQkKThVjw/ptuOzy3vD2MWPD+s0ID2+O+N49cDjrGCY9PgO33zkcRYWliG4fgc6W83Eqv0gWAvLzCrHn70MYOLgv8vMLsW7Nn8jLs+K6Gy6Dv38T+ym82ORXHrmnpuzF/Lk/YuP6HUh67m70T+gOyWaSn/Xx8cKq5VvkGEh9L7agadMAWRzIOHQMKX/ugI+PH64c0A8+via88epCdLa0Res2kbhmwERs2/0hRCYlPz8/NG3qjwMHMrF29Va0ax+JBR8uxV33XIue8R0hYi2tXrEVFRUSBgzsiWbNAuW2CatLXiRAAsYkQAFAGwHgXGIAKFYBwgXA19dX0kQAUIIAqrMAUAAw5mJlr0iABEiABBoXAQoAzgkAR48exZQJsxEY7I3d24/j/QUT0SqiOZ5Jeg+tWrXCmPuuwYibnsRFF1sQGdUCa1Zuw6gxQxESEox7R0/H4KF9EREVgqXfb0XnLq3RrVdrfPnpOlhi4zDuiUTExgzFrSOuwyWXdcPbry/Grf++CoOG9MHYe19BebkNt4xMQJeuHTF39vfo3quDnL3oi0+W49WZj6DN+eHyJl6SRGpjGx554DX0T4jHwQOHsX7dNixanAwTzHhn5lf4e9chXHpFnGwdcCw7D1OeGY1vFq/FTz9swPU398WfWw6iyAr8b+IIfLLoR7SNDkdUVBSGJUzGX3s/wueff44LL7wQYWFhGHvvdDzw0M3IOnwAL77wOr78eiFahIfgpWcXovdFneU2bt28G+MnjETr1i1klwleJEACxiRAAcB1AUBxAVCb/dfyvfuyAKiDABopBoAxlx17RQIkQAIkQAJnJ0ABwHkB4H9jZ+G1WQ/ihWkfI+d4Nj6Y/zSeS16Atm3bol3MeXj5+UX4/NvnZHP9ma8txrdf/orZ857A44+8haemjUaXLh3w71sm4tIruuD+/96CaVPmYe+eI5i7aDJiLxiOWXOmoP+Ablj40TJs+v0PjH3kDsx4aQGuu/4qJAzugbnvfSdv4Cc8dbssCkz632xcfmV33H7nNbIAIMz6s7KO4K3XFuOJyaNx8mQuxj/0Kl6d+RAiIlriqYlv48C+k/h48VQcPJiF56bOwfgn7sBjD82Sy3lw3LU4mlWCwVclYvbcZBxMP4aWEU3RMrw1/jVoGrbtnoc5c+agS5cuyMjIwM7UI3hxxoM4fDgHg68Yh8++eR5ffLICB9KP4N25T8jt+XdiEs5v2wrPv3K/yr3g7POUd5AACXgWAQoArgsANbkAiCwAGRkZwn9Mjvpfw1dtggA6ugDYu2OYIICetZzYWhIgARIgARLQjgAFAOcFgAnj3kfy9DsQ3jIE/fuNwai7rsOp/FK0b98eBdbj2Jd2BFOfHyNXsGrFViRPfh9vv/84Fs77CY9NGommwc0wZfIbuKx/NwwafAlefPZj7Nx+CHMXTcKIxP8h+dmHcGGHCKTtycK7b3+Ma69LwB+b/sLoMTfivPMCMe6/M7Btyx5YukajtMQGa0EJhg6/GCPvGCILAOLEXZjtL/5sJZ6YfJdsiv/uW59h1JirMfruG/HaKwsQHBSKu+8ZhlMFp/DDd6tRUe6Fma9+iYioULSKDJLL2bx5C96YNQl/78xCZOvmdgEgCdv+nov58+fLFgBbt25Fj55dMXDQ5Si0luGJ8S/h9juvxZLvN8DStZ3snuDj44NZb36JDeu2Y94nT1EA0G4ZsyQS0B0BCgANIwC4JQ2g6IpdVZYFACMEAdTdimGDSIAESIAESKCeCFAAcF4AmPy/DzAp+Ta0b98au3fvxahbn4G1oAhvvjMJR45kY+WyrZj1/mPyyfcP367Dy88vxJz5k/De299i/MTbENKsOSZPeAWDh/bGVQMvwfNTF2DXjgx8+PGTSLjsLsx4cwK69WiHjet34tOPv8OI24dj2c/rcc/9NyM0NAhvvPopiotL8cDD16O4qBw7tx+UYwYMGnKR/FlNZMd5Yvyr2LM7A8FBIfD29saRI0cQGdUcH33yHJKfehslRWY888J9KLDmY9abn6F33zg8+/R8PPy/RCQM7obyEn8s+mgpBg7pjTWrN+D86JBKAWBwsmwBoAgAe/bsQdPgFhh5x1AUWktx2w1T8OzLd2Hhhz8jMMgPk5PulGMTPJv0IdL3H8Z78yZSAKinNc5qSKAhCFAAcI8AcCYXACUNYGxsrHZBANUpBZOSkhgEsCFWE+skARIgARIgAQ0JUABwXgC4/66X8crM+9CuXTvZCvPLz1dh7P3PYM68aYi1tMfN1z2JKdPuwoUdW2PCo28jYVBv2bRe+O0/Ne1unBfSDA8/OB2DhsbjmmH98VzyfOzemYl5nzyJ9pFD8PD4Ufj3nQNx3+iX0PeS9rj+xsGY98HXuP+/IxAReR7W/5aCBR8uwT0PXC+frk9+7D08+MgNuGpQpQCwasUmTPzfa/h5zetoGtxcDlJ44kQOxtzxHCZMvgdrVq/H159vxKLFz+L3jVvx6aKl+PybF/Bc0gIcOngUE6aMxP59mZg7ZzGmPvsolv2yFu0uaIGWLVvhX4OfxLZdn1W5AAiRY+qT8zB30ZPYuXMnxoyajGWrFyLnWB5mzvgck5NGy2LFM09/gLHjbsbV11xEAUDDdcyiSEBvBCgAaCMAiCCAYtNvtVpls3/hAlBQUGDLzc2tcgGIjIyUsrKybIoAkJiY6LoA4BgEUHSHAoDelhnbQwIkQAIkQAJ1J0ABwDkBQETy/3nJ7+jbz4IW4ZW57sX1/Te/om10K3TtFoNNG7dgyx+7cPxYIbp2a4/hN1yJzIPH8Oe2neh/5cUICPTF99/9hE6dOuGCC6KxYcMm2Vqge/fuWLp0KUJCQuWgfSHnNcWdd92EU6fysHPHPvTq1QOBTc0oK4O8cd+4PgUnTuTjiisuwcWX9kJgUGVqwd83bMfR7ExcO3wQbLZye8pjLyz9aRUiWrXBpo1/YW9aBrr37IDDWTlyYMILO5yP/PwC2Xph+1/74OVdgWuvG4Suce2wceMWNG/eHH5NgrBy5RrccOM12LY1Fc3DmqJjpwvw+affy+XFxXWByasUPXrGyrEGRDvWrvpLzn4gsgD06NUBTZr4132y8gkSIAGPIUABQBsBQBUHwNHvv8YYAMIFwGKxaC8A0AXAY9YeG0oCJEACJEACZyRAAcA5AUA8Jdid6aqoqLBvuivT8YnNvfrr2Z5399RNmjwb4S2b48GHb0BZWQW8vc1Vbays2+zuJrB8EiABgxKgAKCNAKCkAQwKCrI1a9bMlpmZKYWEhMgWAGFhYVJOTo6jEKBtEEC1C4CSBYAxAAy6atktEiABEiCBRkGAAoD7BAC9T6CN67fD19cX3XpcKGcqcBQ1FMFC7/1g+0iABPRHgAKANgKAowVATS4A6mwAbgsCKLpDFwD9LTS2iARIgARIgATqSoACQOMVAHAWI1G7wUJdpxTvJwESIAFQAHBdAAgICJAKCwsluxXAObkACDFAkxgAShpABgHkaiYBEiABEiABYxGgANCYBYDKvvOk31hrmr0hAT0QoADgugCgOv0XZv1VAoDiAqA++bf/Xg4Y6DYBgC4AelhabAMJkAAJkAAJuEaAAkDjFQC48Xdt7fBpEiCB2glQANBeAIiKiqoWA0AtAERHR9u8vb2ltLQ0xgDgwiQBEiABEiABEqidAAUA9wkA3GBz5ZEACTRWAhQAXBMATCaTTZj/15YFIDQ01HbixAkpLi7OlpKSolgIKPe7ngVAcQEQ6QBFV8QfNMYAaKzLmf0mARIgARIwEgEKABQAjDSf2RcSIAF9EKAA4JoAUFRUpET3Vzb1tcYAiIyMlLKysqru19wFwJ4C0FACQGlpKWw221nT+Zx9OTFdztkZ8Q4SIAESaLwERJo1Hx8fXQGgAOC8AKCrgWRjSIAESEBHBCgAaCcANHgQQCVnbXJysikpKclmhDSApSU2bNz4B3KOnYCfn99pS0eIA/Jlsn+tusPx/w75gE+7X0ersjE1RfJqTL1lX0mABHRMoF371oiNjdVVCykAUADQ1YRkY0iABAxBYP/+A8jLy4PFYkF5ebkh+uRKJ/z9m1R7XKq8sletWjUiISEhtU2bNsGHDh0q8/f3l0/862IB4BAMUNsggMIFQPFnM5ILwMkTp7By5SpkZGTB28v3tLE1m6uf7FdmyuVFAiRAAiRAAnUj0KuPBfHx8XV7yM13UwA4HXBJSQnS0tLQqVMnBAUFuHkEWDwJkAAJGI/Ajh275I1/165dUFxcYrwO1rFHzggAShpA1QZfMfOv5g6gdgGIjY2VhOiixX5VlCHUCJPiAmCkLAB5uVasXr0GmZmHz2iaaXI44FfG3Vz1c0cXAEcLgTrOFN6uEQG6ZmgEksWQAAm4SKBHXwu6d+/uYinaPk4BoGae6enpyM/PR9euFvj6+sLL63RrsoqKCm0Hg6WRAAmQgIcS8Pb2RllZGcTBaU5ODnbt+hs9evTQndtbQ+F1RgCoKQ1g69atpYyMDMd4AHIQwJiYGO2zAKiDABpJALBai/Dr2t9w9Gi2/Ee+tus0AcD+g38EAC20loaalgauV6IAYODRZddIwKMIxMRGyx+I9HRRAKh9NPbtPYDDhw9Xbf4FK3XMoJpEAT2NLdtCAiRAAvVFQLw/CgFACAGBgYGIbtcGzZs3B4XSyhHQSgCoxRqAWQDqOtHLy23IzytAcXGxPGnPdplQ28k+LQDOxo6/JwESIIHGTMA/0A9BQUG6QkABoPbh4AZfV1OVjSEBEvAgAtz4Vx+s+hIANHMBsPv+V7kAiO7YgwHYJEkSR+bpACI8aE7W3NRaTPyr3cxDfo8fZnaABEiABEjgHwIUADgbSIAESIAESMC9BJwRAGqKAaB2AQgLC5NycnLUcQEkzQQAu7m/LACo0IjvDZEFwPXhViwCaGruOkuWQAIkQALGJSDMxx0DyzZ0bykANPQIsH4SIAESIAGjE9BKAKh3FwBFALAHApQFAENZABh95rF/JEACJEACJOBAgAIApwQJkAAJkAAJuJeAMwLA2YIA1mQBYH/GPVkAjJQG0L3DzdJJgARIgARIQL8EKADod2zYMhIgARIgAWMQcEYAONc0gIpVgFuyAKhdACgAGGMyshckQAIkQAKNmwAFgMY9/uw9CZAACZCA+wk4IwDUZAEgfhYSEmIzm822EydOyNH/IyMjpaysLDkWgIgBIHqjRdg6UYZkDwYoE6IA4P6JwhpIgARIgARIwN0EKAC4mzDLJwESIAESaOwEtBQAxKZfvOrNBUAZPAoAjX0ae1L/a0vb6El9YFtJgAQaJwH3B5elANA4ZxZ7TQIkQAIkUH8E3CEAqAICypYAisVAYmKidhYAwprAHgCQFgD1N19YEwmQAAmQAAm4jQAFALehZcEkQAIkQAIkIBNwRQAIDAyUrFarfOrfIFkAKABwFpMACZAACZCAcQhQADDOWLInJEACJEAC+iTgigCg3vS3bt1aysjIkMWAjh07Srt375ZFgejoaJu3t7eUlpYmuc0CwB5bwJaamuprsVjSAUToEzdbRQIkQAIkQAIkUBsBCgCcGyRAAiRAAiTgXgJaCQBqMUDEAIiMjLSlpKRo7wJgD/4nCq5yAaAA4N5JwtJJgARIgARIoD4IUACoD8qsgwRIgARIoDET0EoAaNq0qZSfn+/oDlBNABCcXc4CUJMAwCCAjXkKs+8kQAIkQALnREBOxqO6XP6LfE611ukmCgB1wsWbSYAESIAESKDOBOoqAJhMJlthYaH4FKG8bMHBwbZTp04pm311PADtBQC7iCBbAIje2uMAiO9tkiT5AjCIC4CbosVL7o/iXOdZyAdIgARIgATcT0CHG37HTlMAcP80YA0kQAIkQAKNm0BdBYCioqKqqP7h4eFSdnZ21al/SEiILTc3t1oMAHUWAEHa5Y8fjhYAcqEmkywAGDYGgOOpjStz1uURcKVyPksCJEACJNBwBByFZf0JwhQAGm52sGYSIAESIIHGQcAZASAgIEBSWQGclgVACQIYGRkpZWVl2WJiYuQggJoIAGoLAPFBwfACgJabf61GoHGsDfaSBEiABEignglQAKhn4KyOBEiABEig0RFwRgBQzP/VFgDqGADqLADiYN4tAoDdEqCaC4ARLAAKC4uxf/9+5OcVwNfXt5rJhFkCKioqGt0kZYdJgARIgARcJ2BTWYAJw7kWLUMRFRUFs9ksLOnkV0NfFAAaegRYPwmQAAmQgNEJuCIAqCP/1/K99jEA1C4AyuAYKQhgbm4+Vq1ci4yMrEoBwG4BIDb/4nJdANCfyafRFxn7RwIkQAJ6IGB2ePvv3qcL4uPj9dC0qjZQANDVcLAxJEACJEACBiRQVwFABAEUm33hAiAsAAoLC20FBQXqwH/y93FxcbacnBzZBUD8PzY2VtqxY4frMQAcXQDEiYUiABghCKC1oATLlq1EVuYR+PkFnCYAGHAOskskQAIkQAL1QEBCdQuy7n1i0a1bt3qo+dyroABw7qx4JwmQAAmQAAk4Q6CuAoAIAlhTDIBzSQOYmJionQAgLAGUDicnJ5uSkpIMkQWg4FQxli5dhqzMbPj6NtFeADDRhcCZhcJnSIAESMDTCShxc5R+9OhroQWApw8q208CJEACJEACdSTgjACgSgFYU+q/07IAuC0GgOir3WfRMFkAysrK8Pfff6OkpEQeyioXgDoObK23S95alcRySIAESIAEPIiAZKr0AVCEgFZR4QgLC4WPj49uekELAN0MBRtCAiRAAiRgUAKuCAC1pQFU4gEoWQAUwUBzCwAlYJE9LoAx0wBqngXAMQ2UQWc2u0UCJEACJOBAQP8xYCgAcNKSAAmQAAmQgHsJOCsAtGjRQjKZTFJ2dna1NIAxMTE2Ly8vqUmTJraUlBTtgwCeKQaAEbIAnDbcAqGwb9BKCGj4IM/undEsnQRIgARIwGMJUADw2KFjw0mABEiABDyEgDMCQFhYmCQC/IldaVBQUFUQwJCQEFtubm5VQEC1BYAIAiiQuLz9VGcBUMwYjRQDgAKAh6wcNpMESIAESEBzAhQANEfKAkmABEiABEigGgFnBICzxQBQpQSssgAQAoDFYnFdAFBbACg9MVIawFrnp2IJwAlMAiRAAiRAAgYlQAHAoAPLbpEACZAACeiGgDMCgNoCAEA1FwD7/9Wm/+5JA2i3BFBAGiYIoG5mBhtCAiRAAiRAAvVMgAJAPQNndSRAAiRAAo2OgDMCQG0WAI4uAIolgMgC4OvrK+3YsYMWAI1uhrHDJEACJEACJHCOBCgAnCMo3kYCJEACJEACThLQUgBQrAE6duwo7d69uyoWgEow0FYAEB8URCaARuEC4OQA8zESIAESIAES8BQCFAA8ZaTqr51iTpjN5qr0lfVXszY1KZ9VtSmNpZAACZCA6wScEQAcXQCCg4Ntp06dUsz+HTf+yv8lt6QBFG+sShBAQ2YBcH2MWQIJkAAJkAAJeAQBCgAeMUz13khP30R7evvrfcBZIQmQgFsJOCMA1OQC0LRpUyk/P79aPAB1FgDxjCYCgJIFQIkBQAHArfODhZMACZAACZBAvRGgAFBvqFkRCZAACZBAIyXgqgAg0gCaTKZqFgA1uQCILACaxwCgC0AjnbXsNgmQAAmQgCEJUAAw5LCyUyRAAiRAAjoi4IoAEB4eLmVnZ58xC0B0dLQtPT1duAeIl7YxAFQcmQVAR5OKTSEBEiABEiABZwhQAHCGGp/RMwGbzSbHq1JiGei5rWwbCZBA4yCglQBQkwuAOgtAWlqaNi4AdhFBqAkm8WYqLiUGgCRJvgDSAUQ0juFjL0mABEiABEjAOAQoADTMWFZUVMDLy0vTyr29vauVV15efsbylfvFfeLl+LymjaunwgRXEcDQ398PxcUl9VQrqyEBEiCBMxPQSgAQm/3a0gC6JQuAiAEguiZUVbsoYGMQQE53EiABEiABEvBcAhQAah87cZIsNuliUylefn5+8PX1Rnm5DWVlZec86E2aNEFJyT+bUcFcbLZFmVpeolyr1YpTp04hNDQUot4zXeI+cX9YWBh8fHyqov6Ldvn6+p7WvmPHjsnFtWjRQstma1aWwjU/Px+5ublo1qwZgoKCXC5f4ageQ5cLZQEkQAKNioAzAoDIAmAymaRjx45VRf5XWwAoMQB69eolbd68uSoLQGxsrOsuAEoQQAd3AtkFgBYAjWrusrMkQAIkQAIGI0ABoOYBFRvjb79agUOHDsknyt4+lafKrVqFo0fPrmjfvh3KK0rPOhvy8vKwbt06xMfHIzw8XL5fsaa0H6ictYxzvUF8wJw+/UUsWbIEH3zwASIjI8/46Ny5c/HOO+9g/vz5iIvrKp+YK5vo0tLS0ywURo8eLZcnntPjpXDdvHkzxo0bJ6xVMWDAAKebqpQnxk+IAGIM1ZfW4+d0Q/kgCZCA7gk4IwDUlAWgQSwAVG92FAB0P9XYQBIgARIgARI4MwEKAKfz8fIy4XhOPh57dBoWfroSkQHnw6vcjCO2PJRVFOPiuHZ4e3YyLD0vPKMlgDhVX7NqI25PfB6z5z2BocP7wHqqAma75b/LG0jJDJhsgGRGha0MQUEBmDDhcXy9eA1+Wvo9IqKaQVgx1HT5+TXBs1PfwPRp87B8zQL06dcJVmsRzKJIk2igeM5c7dGhV90HyZyHpb98AZt07hYQbluDSv/tFSiBqrMyjmPhgi8xbPgV6NLF8o8lg2AlX8rX6v1zbKcsAEhmXH311ejbty+mT38eJSWllcz/sYh1W/dYMAnUBwFJUbrqo7JGXEdAgF+13gvskiRlr1q1akRCQkJqmzZtgg8dOlTm7+8vB/IrKiqqOtE/lyCA9je2qiCA/weAt9Vt2O5SOgAAAABJRU5ErkJggg==", + "created": 1764873715847, + "lastRetrieved": 1764873715847 + } + } +} \ No newline at end of file diff --git a/docs/api/README.md b/docs/api/README.md index f5d1b028..2c676c12 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -1,35 +1,321 @@ -# API -The connection between AymurAI's frontend and backend occurs through an Application Programming Interface (API). There are several endpoints that allow communication between the different parts of the software. Calls to these endpoints are made from the frontend to execute different subroutines. +# API Reference +Language: **English** | [Español](../es/api/README.md) -# Endpoints -* `/document-extract`: plain text extraction from .doc or .docx documents -* `/predict`: prediction over a single paragraph -* `/predict-batch`: run prediction over a batch of paragraphs +This document describes the currently mounted public API in `aymurai/api/main.py` + `aymurai/api/core.py`. + +## Base URL and OpenAPI +- Local base URL: `http://localhost:8899` +- Swagger UI: `http://localhost:8899/api/docs` +- OpenAPI JSON: `http://localhost:8899/api/openapi.json` + +## Public Endpoints (Mounted) + +| Method | Path | Purpose | +|---|---|---| +| `GET` | `/api/server/healthcheck` | Service liveness check | +| `GET` | `/api/server/stats/summary` | Runtime CPU/memory stats | +| `POST` | `/api/document-extract` | Deprecated alias of `/api/misc/document-extract` | +| `POST` | `/api/misc/document-extract` | Extract normalized paragraphs from uploaded document | +| `POST` | `/api/anonymizer/predict` | NER prediction for a paragraph | +| `POST` | `/api/anonymizer/disambiguate` | Canonical entity disambiguation + policy merge | +| `POST` | `/api/anonymizer/validation` | Fetch paragraph-level manual validation | +| `POST` | `/api/anonymizer/anonymize-document` | Compile and export anonymized document | +| `POST` | `/api/datapublic/predict/{document_id}` | Predict entities for data-public flow | +| `GET` | `/api/datapublic/validation/document/{document_id}` | Read document-level validation | +| `POST` | `/api/datapublic/validation/document/{document_id}` | Save document-level validation | +| `POST` | `/api/convert/pdf/odt` | Convert PDF to ODT | +| `POST` | `/api/convert/pdf/docx` | Convert PDF to DOCX | +| `POST` | `/api/convert/docx/odt` | Convert DOCX to ODT | +| `POST` | `/api/convert/docx/pdf` | Convert DOCX to PDF | +| `POST` | `/api/convert/odt/pdf` | Convert ODT to PDF | +| `POST` | `/api/convert/odt/docx` | Convert ODT to DOCX | + +## Core Data Contracts + +Note: the JSON snippets below are minimal valid examples. Real payloads may include additional fields depending on the endpoint and processing stage. + +### `TextRequest` +```json +{ + "text": "Acusado: Ramiro Marrón DNI 34.555.666." +} +``` + +### `EntityAttributes` (relevant fields) +```json +{ + "aymurai_label": "PER", + "aymurai_label_subclass": [], + "aymurai_method": "flair", + "aymurai_score": 0.97, + "canonical_entity_id": "a3f8b60f-8e7e-4c8b-9de2-ec8dd3f44c12", + "aymurai_label_instance": 1, + "aymurai_disambiguation": "fuzzy", + "aymurai_anonymize": true +} +``` + +### `DocLabel` +```json +{ + "text": "Ramiro Marrón", + "start_char": 9, + "end_char": 22, + "attrs": { + "aymurai_label": "PER" + } +} +``` + +### `DocumentInformation` +```json +{ + "document": "Acusado: Ramiro Marrón DNI 34.555.666.", + "labels": [ + { + "text": "Ramiro Marrón", + "start_char": 9, + "end_char": 22, + "attrs": { + "aymurai_label": "PER" + } + } + ] +} +``` + +### `LabelPolicy` +```json +{ + "anonymize": true, + "disambiguation": "fuzzy", + "use_subclass_when_available": true +} +``` + +### `RenderPolicy` +```json +{ + "suffix_mode": "auto", + "suffix_threshold": 1 +} +``` + +### `DocumentAnnotations` +```json +{ + "data": [ + { + "document": "...", + "labels": [] + } + ], + "label_policies": { + "PER": { + "anonymize": true, + "disambiguation": "fuzzy", + "use_subclass_when_available": false + } + }, + "render_policy": { + "suffix_mode": "auto", + "suffix_threshold": 1 + } +} +``` + +## Endpoint Details and Examples + +### Server + +#### `GET /api/server/healthcheck` +- Response `200`: + +```json +{"status": "ok"} +``` -# Deployment -This project is deployed using [Docker](https://www.docker.com/) , and the images are available in the following registry: ```bash -registry.gitlab.com/collective.ai/datagenero-public/aymurai-api:prod +curl -s http://localhost:8899/api/server/healthcheck ``` -You can a production ready instance of the API by running: + +#### `GET /api/server/stats/summary` +- Response `200` (shape): + +```json +{ + "is_docker": true, + "cpu_core_limit": 4, + "cpu_usage_percent": 0.0, + "memory_limit_mb": 4096.0, + "memory_usage_mb": 823.5 +} +``` + ```bash -docker run -d --rm -p 8899:8899 \ - registry.gitlab.com/collective.ai/datagenero-public/aymurai-api:prod +curl -s http://localhost:8899/api/server/stats/summary ``` -this will start a container with the API running on port 8899 on your localhost. The API is documented using OpenAPI and can be accessed at `http://localhost:8899/docs` -Once it is deployed, it doesn't requere a internet connection to work. If you need to deployed in a closed network you can port the image using + +### Document Extraction + +#### `POST /api/misc/document-extract` +#### `POST /api/document-extract` (deprecated alias) +- Request: `multipart/form-data` with `file` +- Supported MIME types in extraction flow: DOCX, ODT, PDF +- Response `200`: + +```json +{ + "document": ["Paragraph 1", "Paragraph 2"], + "document_id": "f2b25507-cf88-5b11-8f2a-c0b6f940b7f8" +} +``` + ```bash -docker image save registry.gitlab.com/collective.ai/datagenero-public/aymurai-api:prod -o aymurai-api.tar +curl -s -X POST \ + -F "file=@/resources/data/sample/document-01.docx" \ + http://localhost:8899/api/misc/document-extract ``` -and then transfer the image to the target machine and load it using + +Common errors: +- `504` extraction timeout +- `500` extractor/internal errors + +### Anonymizer + +#### `POST /api/anonymizer/predict` +- Request body: `TextRequest` +- Query param: `use_cache=true|false` (default `true`) +- Response `200`: `DocumentInformation` + ```bash -docker load -i aymurai-api.tar +curl -s -X POST "http://localhost:8899/api/anonymizer/predict?use_cache=true" \ + -H "Content-Type: application/json" \ + -d '{"text":"Acusado: Ramiro Marrón DNI 34.555.666."}' ``` -For more information about docker deployment please refer to the [docker documentation](https://docs.docker.com/). -You also can contact us at aymurai@datagenero.org and we will be happy to help you. +#### `POST /api/anonymizer/disambiguate` +- Request body: + +```json +{ + "paragraphs": [ + { + "document": "Acusado: Ramiro Marrón DNI 34.555.666.", + "labels": [] + } + ], + "label_policies": { + "PER": { + "anonymize": true, + "disambiguation": "fuzzy", + "use_subclass_when_available": false + } + } +} +``` + +- Response `200`: `DocumentAnnotations` (with `data` and effective `label_policies`) + +```bash +curl -s -X POST http://localhost:8899/api/anonymizer/disambiguate \ + -H "Content-Type: application/json" \ + -d '{"paragraphs":[{"document":"Acusado: Ramiro Marrón DNI 34.555.666.","labels":[]}],"label_policies":{"PER":{"anonymize":true,"disambiguation":"fuzzy"}}}' +``` + +#### `POST /api/anonymizer/validation` +- Request body: `TextRequest` +- Response `200`: `list[DocLabel] | null` + +```bash +curl -s -X POST http://localhost:8899/api/anonymizer/validation \ + -H "Content-Type: application/json" \ + -d '{"text":"Acusado: Ramiro Marrón DNI 34.555.666."}' +``` + +#### `POST /api/anonymizer/anonymize-document` +- Request: `multipart/form-data` + - `file`: original document (`.docx` or `.pdf`) + - `annotations`: JSON string serialized from `DocumentAnnotations` +- Response `200`: anonymized `.odt` for DOCX input or anonymized `.pdf` for PDF input + +```bash +curl -X POST http://localhost:8899/api/anonymizer/anonymize-document \ + -F "file=@/resources/data/sample/document-01.docx" \ + -F 'annotations={"data":[{"document":"Acusado: Ramiro Marrón DNI 34.555.666.","labels":[]}],"label_policies":{"PER":{"anonymize":true,"disambiguation":"fuzzy"}},"render_policy":{"suffix_mode":"auto","suffix_threshold":1}}' +``` + +Common errors: +- `400` invalid form payload, unsupported input format, or invalid document +- `500` conversion/anonymization failures + +### Data-Public + +#### `POST /api/datapublic/predict/{document_id}` +- Path param: `document_id` (`UUID5`) +- Request body: `TextRequest` +- Query param: `use_cache=true|false` (default `true`) +- Response `200`: `DocumentInformation` + +```bash +curl -s -X POST "http://localhost:8899/api/datapublic/predict/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc?use_cache=true" \ + -H "Content-Type: application/json" \ + -d '{"text":"Buenos Aires, 17 de noviembre de 2024"}' +``` + +#### `GET /api/datapublic/validation/document/{document_id}` +- Response `200`: object or `null` +- Response `404`: document not found + +```bash +curl -s http://localhost:8899/api/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc +``` + +#### `POST /api/datapublic/validation/document/{document_id}` +- Request body: free-form JSON object (stored as document-level validation) +- Response `200`: empty body + +```bash +curl -s -X POST http://localhost:8899/api/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc \ + -H "Content-Type: application/json" \ + -d '{"materia":"penal","violencia_de_genero":"si"}' +``` + +### Document Conversion + +All conversion endpoints use `multipart/form-data` with a `file` field. + +| Method | Path | Input | Output | +|---|---|---|---| +| `POST` | `/api/convert/pdf/odt` | `.pdf` | `.odt` | +| `POST` | `/api/convert/pdf/docx` | `.pdf` | `.docx` | +| `POST` | `/api/convert/docx/odt` | `.docx` | `.odt` | +| `POST` | `/api/convert/docx/pdf` | `.docx` | `.pdf` | +| `POST` | `/api/convert/odt/pdf` | `.odt` | `.pdf` | +| `POST` | `/api/convert/odt/docx` | `.odt` | `.docx` | + +For PDF input endpoints, optional query param: +- `backend=libreoffice|pandoc` (default: `libreoffice`) + +Example: + +```bash +curl -X POST "http://localhost:8899/api/convert/pdf/docx?backend=libreoffice" \ + -F "file=@input.pdf" -o output.docx +``` + +Common errors: +- `400` unsupported input extension +- `500` conversion tool failure + +## Legacy / Not Public (Not Mounted) +The following route modules exist in code but are not included in `core.router` at runtime: -# Source -The api source code is written in top of FastAPI and can be found in [here](../../api/). +- `aymurai/api/endpoints/routers/datapublic/dataset.py` + - includes `/api/datapublic/dataset/*` CRUD/batch routes, but router is not mounted. +- `aymurai/api/endpoints/routers/anonymizer/database.py` + - `/api/anonymizer/database/*` routes exist, include is commented out. +- `aymurai/api/endpoints/routers/database/*` + - additional DB admin routes exist, but no mounting in `core.router`. -Please follow the [code of conduct](../CODE_OF_CONDUCT.md) and [contributing guidelines](../CONTRIBUTING.md). \ No newline at end of file +Treat these as legacy/internal code paths until explicitly exposed in the public router. diff --git a/docs/data/en/entities-table.md b/docs/data/en/entities-table.md deleted file mode 100644 index 1037b24d..00000000 --- a/docs/data/en/entities-table.md +++ /dev/null @@ -1,32 +0,0 @@ -# Entities -| Entity | Description | -|-----------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| ART_INFRINGIDO | Article(s) of the infraction(s) in the case. | -| CONDUCTA | Indicates the action relating to the crime, contravention, or misdemeanor described in the article infringed. | -| CONDUCTA_DESCRIPCION | Indicates that the contravention or offence infringed has some particularity, such as being aggravated by some causality. | -| DECISION | Fragment of text denoting a decision taken by the court regarding the case. | -| DETALLE | Specifies OBJETO_DE_LA_RESOLUCION as to what was resolved. | -| EDAD_AL_MOMENTO_DEL_HECHO | Indicates the age of the person at the time of the event. | -| FECHA_DE_NACIMIENTO | Date of birth. | -| FECHA_DEL_HECHO | Date on which the reported event occurred. | -| FECHA_RESOLUCION | Day of resolution. In the case of oral hearings, it is the day of the beginning of the hearing. | -| FRASES_AGRESION | Transcription of the phrases described by the victim as the verbal aggression suffered and that are part of the facts of the case. Applies to cases of verbal violence. | -| GENERO | Gender. | -| HIJOS_HIJAS_EN_COMUN | Indicates whether the accused person and the complainant have children in common. | -| HORA_DE_CIERRE | Time of end of court hearing. | -| HORA_DE_INICIO | Court hearing start time. | -| LUGAR_DEL_HECHO | Indicates the physical place (environment or public road) where the facts occurred or if it was committed by technological means. | -| MODALIDAD_DE_LA_VIOLENCIA | The way in which the different types of violence manifest themselves. For example: domestic, institutional, media, labor, reproductive freedom, obstetric, in public or private spaces, and political and public violence. | -| N_EXPTE_EJE | Case identifier number. | -| NACIONALIDAD | Nationality. | -| NIVEL_INSTRUCCION | Level of formal education attained by the individual. | -| NOMBRE | Name. | -| OBJETO_DE_LA_RESOLUCION | About what was resolved. | -| PERSONA_ACUSADA_NO_DETERMINADA | In case the accused person is not a natural person or is not determined. For example: legal entity, social network user, etc. | -| RELACION_Y_TIPO_ENTRE_ACUSADO/A_Y_DENUNCIANTE | Indicates the type of relationship the accused person has with the complainant. | -| TIPO_DE_RESOLUCION | Interlocutory decisions are those that define a specific issue during the process of the proceeding, or final decisions are those that put an end to the process of the case. | -| VIOLENCIA_DE_GENERO | If the fact under investigation is within a context of gender violence. | -# Subcategories -The majority of the entities described above have subcategories acording their use in the criminal court and the type of information they contain. You can check a more complete description [here](https://docs.google.com/document/d/123B9T2abCEqBaxxOl5c7HBJZRdIMtKDWo6IKHIVil04/edit) (Spanish version) - -A comprehensive list of all entities and their subcategory can be found [here](https://docs.google.com/spreadsheets/d/1FTRtltYJrGJ6huQd0ih-QBZfqo5LGamVFXvXKcmX9-k/edit#gid=0) \ No newline at end of file diff --git a/docs/data/es/entities-table.md b/docs/data/es/entities-table.md deleted file mode 100644 index 1f4b9301..00000000 --- a/docs/data/es/entities-table.md +++ /dev/null @@ -1,27 +0,0 @@ -| Entidad | Descripcion | -|-----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| ART_INFRINGIDO | Artículo/s de la/s infracción/es en el caso | -| CONDUCTA | Indica la acción relativa al delito, la contravención, o la falta que aparece descripta en el artículo infringido; | -| CONDUCTA_DESCRIPCION | Indica la contravención o la falta infringida tiene alguna particularidad, como puede ser que se encuentra agravada por alguna causal | -| DECISION | Fragmento de texto que denota una decision tomada por el juzgado respecto al caso. | -| DETALLE | Especifica OBJETO_DE_LA_RESOLUCIÓN respecto a qué se resolvió | -| EDAD_AL_MOMENTO_DEL_HECHO | Indica la edad de la persona al momento del hecho | -| FECHA_DE_NACIMIENTO | Fecha de nacimiento | -| FECHA_DEL_HECHO | Fecha en que sucedio el hecho denunciado | -| FECHA_RESOLUCION | Día de la resolución. En caso de las audiencias orales es el día de su inicio | -| FRASES_AGRESION | Transcripción de las frases descriptas por la víctima como la agresión verbal sufrida y que son parte de los hechos del caso. Aplica para los casos de violencia verbal; | -| GENERO | Genero | -| HIJOS_HIJAS_EN_COMUN | indica si la persona acusada y la denunciante tienen hijos/as en común | -| HORA_DE_CIERRE | Hora de finalización de la audiencia | -| HORA_DE_INICIO | Hora de inicio de la audiencia | -| LUGAR_DEL_HECHO | Indica lugar físico (ambiente o vía pública) donde ocurrieron los hechos o si fue cometido mediante medios tecnológicos | -| MODALIDAD_DE_LA_VIOLENCIA | Forma en que se manifiestan los distintos tipos de violencia | -| N_EXPTE_EJE | Numeroo identificador del caso | -| NACIONALIDAD | Nacioalidad | -| NIVEL_INSTRUCCION | Nivel de estudios formales alcanzados por la persona | -| NOMBRE | Nombre | -| OBJETO_DE_LA_RESOLUCION | Sobre qué se resolvió | -| PERSONA_ACUSADA_NO_DETERMINADA | En el caso de que la persona acusada no sea una persona física o no esté determinada. Por ejemplo: persona jurídica, usuario de red social, etc. | -| RELACION_Y_TIPO_ENTRE_ACUSADO/A_Y_DENUNCIANTE | Indica el tipo de vínculo que tiene la persona acusada con la denunciante | -| TIPO_DE_RESOLUCION | Interlocutorias son aquellas que definen una cuestión concreta durante la tramitación del proceso, o definitivas son aquellas resoluciones que ponen fin al proceso de la causa | -| VIOLENCIA_DE_GENERO | Si el hecho objeto de investigación se encuentra dentro de un contexto de violencia de género. | diff --git a/docs/database/README.md b/docs/database/README.md index 387ea6ed..555524e4 100644 --- a/docs/database/README.md +++ b/docs/database/README.md @@ -1,66 +1,113 @@ -# Descripción del nuevo flujo de integración en AymurAI - -En este documento, describimos brevemente el nuevo flujo de integración entre el _front-end_ y el _back-end_ de AymurAI. - -## Actualización principal del back-end - -Se implementó un motor de base de datos que permite persistir en disco tanto las predicciones generadas por los modelos como las validaciones manuales realizadas desde la interfaz gráfica. Esta base de datos no sólo funciona como un caché para las predicciones, sino que además permite a les usuaries recuperar la última validación manual en caso de necesitar reprocesar un documento específico. - -A continuación, se detalla el nuevo flujo para cada _pipeline_ de AymurAI: +# Internal Database +Language: **English** | [Español](../es/database/README.md) + +This document describes the internal persistence used by the API runtime. + +## Engine and Migrations +- ORM: `SQLModel` (`sqlalchemy` backend) +- Migration tool: Alembic +- Runtime setting: `SQLALCHEMY_DATABASE_URI` +- Default DB URI: `sqlite:////resources/cache/sqlite/database.db` +- Startup behavior: + 1. API checks DB connectivity. + 2. If SQLite file does not exist, it creates parent directories. + 3. API runs `alembic upgrade head` on startup. + +Related code: +- `aymurai/settings.py` +- `aymurai/api/startup/database.py` +- `aymurai/api/main.py` +- `aymurai/database/versions/13f78d08e925_create_database.py` + +## ER Diagram +![Database ER diagram](schema.png) + +Editable source: [`schema.mmd`](schema.mmd) + +## Tables + +### `anonymization_document` +| Column | Type | Nullable | Notes | +|---|---|---|---| +| `id` | `UUID` | no | Primary key | +| `created_at` | `DATETIME` | no | Server default `CURRENT_TIMESTAMP` | +| `updated_at` | `DATETIME` | yes | Updated on row changes | +| `name` | `TEXT` | no | Original filename | + +### `anonymization_paragraph` +| Column | Type | Nullable | Notes | +|---|---|---|---| +| `id` | `UUID` | no | Primary key, derived from paragraph text hash | +| `text` | `TEXT` | no | Normalized paragraph text | +| `prediction` | `JSON` | yes | Model predictions (`list[DocLabel]`) | +| `validation` | `JSON` | yes | Manual labels (`list[DocLabel]`) | +| `created_at` | `DATETIME` | no | Server default `CURRENT_TIMESTAMP` | +| `updated_at` | `DATETIME` | yes | Updated on row changes | + +### `anonymization_document_paragraph` +| Column | Type | Nullable | Notes | +|---|---|---|---| +| `id` | `UUID` | no | Link row identifier | +| `document_id` | `UUID` | no | FK -> `anonymization_document.id` | +| `paragraph_id` | `UUID` | no | FK -> `anonymization_paragraph.id` | +| `order` | `INTEGER` | yes | Paragraph order in source document | + +Primary key is composite over `id`, `document_id`, `paragraph_id`. + +### `datapublic_document` +| Column | Type | Nullable | Notes | +|---|---|---|---| +| `id` | `UUID` | no | Primary key (document identifier) | +| `prediction` | `JSON` | yes | Reserved document-level prediction payload; not written by the current public router | +| `validation` | `JSON` | yes | Document-level validation payload | +| `created_at` | `DATETIME` | no | Server default `CURRENT_TIMESTAMP` | +| `updated_at` | `DATETIME` | yes | Updated on row changes | + +### `datapublic_paragraph` +| Column | Type | Nullable | Notes | +|---|---|---|---| +| `id` | `UUID` | no | Primary key, derived from paragraph text hash | +| `text` | `TEXT` | no | Normalized paragraph text | +| `prediction` | `JSON` | yes | Model predictions (`list[DocLabel]`) | +| `validation` | `JSON` | yes | Reserved for paragraph-level validation; public route is currently commented legacy logic | +| `created_at` | `DATETIME` | no | Server default `CURRENT_TIMESTAMP` | +| `updated_at` | `DATETIME` | yes | Updated on row changes | + +### `datapublic_document_paragraph` +| Column | Type | Nullable | Notes | +|---|---|---|---| +| `id` | `UUID` | no | Link row identifier | +| `document_id` | `UUID` | no | FK -> `datapublic_document.id` | +| `paragraph_id` | `UUID` | no | FK -> `datapublic_paragraph.id` | +| `order` | `INTEGER` | yes | Paragraph order in source document | + +Primary key is composite over `id`, `document_id`, `paragraph_id`. + +## Endpoint to Persistence Mapping ### Anonymizer - -1. **Consulta inicial en la base de datos (_endpoint_ `/anonymizer/validation`):** - - Tras la extracción del texto del documento, se verifica si existe una validación guardada para cada uno de sus párrafos. El identificador utilizado es un _hash_ único generado a partir del contenido textual del párrafo. - - - **Si existe una validación previa en la tabla `anonymization_paragraph`:** se retorna dicha validación. - - **Si no existe una validación previa:** debe dispararse el flujo de predicciones consumiendo el _endpoint_ correspondiente. - -2. **Consulta de predicciones (_endpoint_ `/anonymizer/predict`):** - - - **Si la predicción para el párrafo ya existe en la tabla `anonymization_paragraph`:** se retorna. - - **Si no existe:** se ejecuta el modelo de predicción, y los resultados se retornan y se persisten en la tabla `anonymization_paragraph`. - - > **Formato de etiquetas:** tanto las validaciones manuales como las predicciones se estructuran utilizando el formato actual de las etiquetas de AymurAI. - -3. **Interfaz gráfica y validación manual:** - - Las etiquetas retornadas se muestran en la interfaz gráfica, donde pueden ser validadas manualmente. Una vez finalizada la validación, se procede con la anonimización del documento (_endpoint_ `/anonymizer/anonymize-document`). - -4. **Persistencia de validaciones:** - - Al finalizar el proceso, las validaciones se escriben en la tabla `anonymization_paragraph`, ya sea creando nuevos registros para párrafos nuevos o actualizando registros existentes para párrafos previamente procesados. Esta escritura es realizada al inicio del flujo de `anonymize-document`, luego de que se indique desde la UI que el documento está listo para su anonimización. Además, en la tabla `anonymization_document` se escribe la referencia al ID del documento y el nombre del archivo correspondiente. Asimismo, el ID del documento permite recuperar los párrafos y su orden en el documento a través de la tabla `anonymization_document_paragraph`. - -### Data Public - -El flujo para el _pipeline_ de Data Public es similar al del Anonymizer, pero presenta diferencias importantes en cuanto a las validaciones, ya que estas no comparten el mismo formato que las predicciones originales del modelo. En este caso, las validaciones se realizan a nivel general del documento (no por párrafo) y se llevan a cabo manualmente en la sección lateral derecha de la UI, donde se definen los campos que serán volcados al dataset público. A diferencia del _pipeline_ de anonimización, las predicciones siempre son necesarias para resaltar los campos detectados en el texto, incluso cuando ya existen validaciones realizadas, ya que estas no son modificaciones directas sobre las entidades reconocidas por el modelo. - -1. **Consulta inicial en la base de datos (_endpoint_ `/datapublic/dataset/{document_id}`):** - - Tras la extracción del texto del documento, se verifica si existe una validación general para el documento. - - - **Si existe una validación previa para el documento en la tabla `datapublic_dataset`:** se retorna dicha validación. - -2. **Flujo de predicciones (_endpoint_ `/datapublic/predict`):** - - A su vez, se consulta si existen predicciones guardadas para cada uno de los párrafos: - - - **Si existe una predicción previa para cada párrafo en la tabla `datapublic_paragraph`:** se retorna dicha predicción. - - - **Si no existe:** se ejecuta el modelo de predicción, y los resultados se retornan y se persisten en la tabla `datapublic_paragraph`. - - > **Formato de etiquetas:** las predicciones se estructuran utilizando el formato actual de las etiquetas de AymurAI. Las validaciones, sin embargo, tienen un formato distinto, **equivalente al de las filas del dataset público**. Es necesario tener presente esto para poder integrar correctamente con el _front-end_ los campos validados persistidos en la base de datos. - -3. **Interfaz gráfica y validación manual:** - - Una vez finalizadas las consultas a la base de datos y el flujo de predicciones, las etiquetas retornadas se muestran en la interfaz gráfica, donde pueden ser revisadas y validadas manualmente. Posteriormente, las nuevas validaciones se persisten en la tabla `datapublic_dataset` haciendo un _post_ contra el _endpoint_ `/datapublic/dataset`, ya sea creando nuevos registros para documentos nuevos o actualizando registros existentes para documentos previamente procesados. Esta escritura debe realizarse una vez que desde la UI se confirma que los datos extraídos del documento son correctos. - - -#### Requerimientos a la UI - -Para la correcta persistencia de los datos (por ejemplo, para hacer evaluaciones o futuros entrenamientos de los modelos), se necesita que la UI envíe los datos corregidos a la API. Esto puede ser resumido en un _endpoint_ de "compilación" al final del proceso de validación. Este _endpoint_ sería equivalente al `anonymization/anonymize-document` - -## Esquema de la base de datos - -![schema](schema.jpg) +- `POST /api/anonymizer/predict` + - Reads `anonymization_paragraph` by paragraph UUID. + - Writes `anonymization_paragraph.prediction` when cache is enabled. +- `POST /api/anonymizer/disambiguate` + - Writes disambiguated predictions to `anonymization_paragraph.prediction`. +- `POST /api/anonymizer/validation` + - Reads `anonymization_paragraph.validation`. +- `POST /api/anonymizer/anonymize-document` + - Writes `anonymization_paragraph.validation`. + - Creates `anonymization_document` keyed by uploaded file content hash. + - Creates link rows in `anonymization_document_paragraph`. + +### Data-public +- `POST /api/datapublic/predict/{document_id}` + - Uses caller-provided `document_id` as the document primary key. + - Ensures `datapublic_document` exists when `use_cache=true`. + - Writes `datapublic_paragraph.prediction` when `use_cache=true`. + - Writes link row in `datapublic_document_paragraph` when `use_cache=true`. +- `GET /api/datapublic/validation/document/{document_id}` + - Reads `datapublic_document.validation`. +- `POST /api/datapublic/validation/document/{document_id}` + - Upserts `datapublic_document.validation`. + +## Legacy Note +Route modules for dataset CRUD (`/api/datapublic/dataset/*`) exist in code but are not mounted in the public router. Do not treat them as active public API until exposed in `core.router`. diff --git a/docs/database/schema.excalidraw b/docs/database/schema.excalidraw new file mode 100644 index 00000000..111dd9fe --- /dev/null +++ b/docs/database/schema.excalidraw @@ -0,0 +1,56 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", + "elements": [ + { + "id": "UnZYOKsPO14ownJjIhB0K", + "type": "image", + "x": 216.9140625, + "y": 240.5078125, + "width": 968.171875, + "height": 397.984375, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a2", + "roundness": null, + "seed": 810215012, + "version": 5, + "versionNonce": 845707748, + "isDeleted": false, + "boundElements": [], + "updated": 1772457952595, + "link": null, + "locked": false, + "status": "saved", + "fileId": "X7nK5-PoRG7EVUWSedgsz", + "scale": [ + 1, + 1 + ], + "crop": null + } + ], + "appState": { + "gridSize": 20, + "gridStep": 5, + "gridModeEnabled": false, + "viewBackgroundColor": "#ffffff" + }, + "files": { + "X7nK5-PoRG7EVUWSedgsz": { + "id": "X7nK5-PoRG7EVUWSedgsz", + "mimeType": "image/svg+xml", + "dataURL": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGFyaWEtcm9sZWRlc2NyaXB0aW9uPSJlciIgcm9sZT0iZ3JhcGhpY3MtZG9jdW1lbnQgZG9jdW1lbnQiIHZpZXdCb3g9IjAgMCA5NjguMTgxNzAxNjYwMTU2MiAzOTgiIHN0eWxlPSJtYXgtd2lkdGg6IDk2OC4xODE3MDE2NjAxNTYycHg7IiB3aWR0aD0iOTY4LjE3MTg3NSIgaWQ9Im1lcm1haWQtdG8tZXhjYWxpZHJhdyIgaGVpZ2h0PSIzOTcuOTg0Mzc1Ij48c3R5bGU+I21lcm1haWQtdG8tZXhjYWxpZHJhd3tmb250LWZhbWlseToidHJlYnVjaGV0IG1zIix2ZXJkYW5hLGFyaWFsLHNhbnMtc2VyaWY7Zm9udC1zaXplOjI1cHg7ZmlsbDojMzMzO30jbWVybWFpZC10by1leGNhbGlkcmF3IC5lcnJvci1pY29ue2ZpbGw6IzU1MjIyMjt9I21lcm1haWQtdG8tZXhjYWxpZHJhdyAuZXJyb3ItdGV4dHtmaWxsOiM1NTIyMjI7c3Ryb2tlOiM1NTIyMjI7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgLmVkZ2UtdGhpY2tuZXNzLW5vcm1hbHtzdHJva2Utd2lkdGg6MnB4O30jbWVybWFpZC10by1leGNhbGlkcmF3IC5lZGdlLXRoaWNrbmVzcy10aGlja3tzdHJva2Utd2lkdGg6My41cHg7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgLmVkZ2UtcGF0dGVybi1zb2xpZHtzdHJva2UtZGFzaGFycmF5OjA7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgLmVkZ2UtcGF0dGVybi1kYXNoZWR7c3Ryb2tlLWRhc2hhcnJheTozO30jbWVybWFpZC10by1leGNhbGlkcmF3IC5lZGdlLXBhdHRlcm4tZG90dGVke3N0cm9rZS1kYXNoYXJyYXk6Mjt9I21lcm1haWQtdG8tZXhjYWxpZHJhdyAubWFya2Vye2ZpbGw6IzMzMzMzMztzdHJva2U6IzMzMzMzMzt9I21lcm1haWQtdG8tZXhjYWxpZHJhdyAubWFya2VyLmNyb3Nze3N0cm9rZTojMzMzMzMzO30jbWVybWFpZC10by1leGNhbGlkcmF3IHN2Z3tmb250LWZhbWlseToidHJlYnVjaGV0IG1zIix2ZXJkYW5hLGFyaWFsLHNhbnMtc2VyaWY7Zm9udC1zaXplOjI1cHg7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgLmVudGl0eUJveHtmaWxsOiNFQ0VDRkY7c3Ryb2tlOiM5MzcwREI7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgLmF0dHJpYnV0ZUJveE9kZHtmaWxsOiNmZmZmZmY7c3Ryb2tlOiM5MzcwREI7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgLmF0dHJpYnV0ZUJveEV2ZW57ZmlsbDojZjJmMmYyO3N0cm9rZTojOTM3MERCO30jbWVybWFpZC10by1leGNhbGlkcmF3IC5yZWxhdGlvbnNoaXBMYWJlbEJveHtmaWxsOmhzbCg4MCwgMTAwJSwgOTYuMjc0NTA5ODAzOSUpO29wYWNpdHk6MC43O2JhY2tncm91bmQtY29sb3I6aHNsKDgwLCAxMDAlLCA5Ni4yNzQ1MDk4MDM5JSk7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgLnJlbGF0aW9uc2hpcExhYmVsQm94IHJlY3R7b3BhY2l0eTowLjU7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgLnJlbGF0aW9uc2hpcExpbmV7c3Ryb2tlOiMzMzMzMzM7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgLmVudGl0eVRpdGxlVGV4dHt0ZXh0LWFuY2hvcjptaWRkbGU7Zm9udC1zaXplOjE4cHg7ZmlsbDojMzMzO30jbWVybWFpZC10by1leGNhbGlkcmF3ICNNRF9QQVJFTlRfU1RBUlR7ZmlsbDojZjVmNWY1IWltcG9ydGFudDtzdHJva2U6IzMzMzMzMyFpbXBvcnRhbnQ7c3Ryb2tlLXdpZHRoOjE7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgI01EX1BBUkVOVF9FTkR7ZmlsbDojZjVmNWY1IWltcG9ydGFudDtzdHJva2U6IzMzMzMzMyFpbXBvcnRhbnQ7c3Ryb2tlLXdpZHRoOjE7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgOnJvb3R7LS1tZXJtYWlkLWZvbnQtZmFtaWx5OiJ0cmVidWNoZXQgbXMiLHZlcmRhbmEsYXJpYWwsc2Fucy1zZXJpZjt9PC9zdHlsZT48Zy8+PGRlZnM+PG1hcmtlciBvcmllbnQ9ImF1dG8iIG1hcmtlckhlaWdodD0iMjQwIiBtYXJrZXJXaWR0aD0iMTkwIiByZWZZPSI3IiByZWZYPSIwIiBpZD0iTURfUEFSRU5UX1NUQVJUIj48cGF0aCBkPSJNIDE4LDcgTDksMTMgTDEsNyBMOSwxIFoiLz48L21hcmtlcj48L2RlZnM+PGRlZnM+PG1hcmtlciBvcmllbnQ9ImF1dG8iIG1hcmtlckhlaWdodD0iMjgiIG1hcmtlcldpZHRoPSIyMCIgcmVmWT0iNyIgcmVmWD0iMTkiIGlkPSJNRF9QQVJFTlRfRU5EIj48cGF0aCBkPSJNIDE4LDcgTDksMTMgTDEsNyBMOSwxIFoiLz48L21hcmtlcj48L2RlZnM+PGRlZnM+PG1hcmtlciBvcmllbnQ9ImF1dG8iIG1hcmtlckhlaWdodD0iMTgiIG1hcmtlcldpZHRoPSIxOCIgcmVmWT0iOSIgcmVmWD0iMCIgaWQ9Ik9OTFlfT05FX1NUQVJUIj48cGF0aCBkPSJNOSwwIEw5LDE4IE0xNSwwIEwxNSwxOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJncmF5Ii8+PC9tYXJrZXI+PC9kZWZzPjxkZWZzPjxtYXJrZXIgb3JpZW50PSJhdXRvIiBtYXJrZXJIZWlnaHQ9IjE4IiBtYXJrZXJXaWR0aD0iMTgiIHJlZlk9IjkiIHJlZlg9IjE4IiBpZD0iT05MWV9PTkVfRU5EIj48cGF0aCBkPSJNMywwIEwzLDE4IE05LDAgTDksMTgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iZ3JheSIvPjwvbWFya2VyPjwvZGVmcz48ZGVmcz48bWFya2VyIG9yaWVudD0iYXV0byIgbWFya2VySGVpZ2h0PSIxOCIgbWFya2VyV2lkdGg9IjMwIiByZWZZPSI5IiByZWZYPSIwIiBpZD0iWkVST19PUl9PTkVfU1RBUlQiPjxjaXJjbGUgcj0iNiIgY3k9IjkiIGN4PSIyMSIgZmlsbD0id2hpdGUiIHN0cm9rZT0iZ3JheSIvPjxwYXRoIGQ9Ik05LDAgTDksMTgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iZ3JheSIvPjwvbWFya2VyPjwvZGVmcz48ZGVmcz48bWFya2VyIG9yaWVudD0iYXV0byIgbWFya2VySGVpZ2h0PSIxOCIgbWFya2VyV2lkdGg9IjMwIiByZWZZPSI5IiByZWZYPSIzMCIgaWQ9IlpFUk9fT1JfT05FX0VORCI+PGNpcmNsZSByPSI2IiBjeT0iOSIgY3g9IjkiIGZpbGw9IndoaXRlIiBzdHJva2U9ImdyYXkiLz48cGF0aCBkPSJNMjEsMCBMMjEsMTgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iZ3JheSIvPjwvbWFya2VyPjwvZGVmcz48ZGVmcz48bWFya2VyIG9yaWVudD0iYXV0byIgbWFya2VySGVpZ2h0PSIzNiIgbWFya2VyV2lkdGg9IjQ1IiByZWZZPSIxOCIgcmVmWD0iMTgiIGlkPSJPTkVfT1JfTU9SRV9TVEFSVCI+PHBhdGggZD0iTTAsMTggUSAxOCwwIDM2LDE4IFEgMTgsMzYgMCwxOCBNNDIsOSBMNDIsMjciIGZpbGw9Im5vbmUiIHN0cm9rZT0iZ3JheSIvPjwvbWFya2VyPjwvZGVmcz48ZGVmcz48bWFya2VyIG9yaWVudD0iYXV0byIgbWFya2VySGVpZ2h0PSIzNiIgbWFya2VyV2lkdGg9IjQ1IiByZWZZPSIxOCIgcmVmWD0iMjciIGlkPSJPTkVfT1JfTU9SRV9FTkQiPjxwYXRoIGQ9Ik0zLDkgTDMsMjcgTTksMTggUTI3LDAgNDUsMTggUTI3LDM2IDksMTgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iZ3JheSIvPjwvbWFya2VyPjwvZGVmcz48ZGVmcz48bWFya2VyIG9yaWVudD0iYXV0byIgbWFya2VySGVpZ2h0PSIzNiIgbWFya2VyV2lkdGg9IjU3IiByZWZZPSIxOCIgcmVmWD0iMTgiIGlkPSJaRVJPX09SX01PUkVfU1RBUlQiPjxjaXJjbGUgcj0iNiIgY3k9IjE4IiBjeD0iNDgiIGZpbGw9IndoaXRlIiBzdHJva2U9ImdyYXkiLz48cGF0aCBkPSJNMCwxOCBRMTgsMCAzNiwxOCBRMTgsMzYgMCwxOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJncmF5Ii8+PC9tYXJrZXI+PC9kZWZzPjxkZWZzPjxtYXJrZXIgb3JpZW50PSJhdXRvIiBtYXJrZXJIZWlnaHQ9IjM2IiBtYXJrZXJXaWR0aD0iNTciIHJlZlk9IjE4IiByZWZYPSIzOSIgaWQ9IlpFUk9fT1JfTU9SRV9FTkQiPjxjaXJjbGUgcj0iNiIgY3k9IjE4IiBjeD0iOSIgZmlsbD0id2hpdGUiIHN0cm9rZT0iZ3JheSIvPjxwYXRoIGQ9Ik0yMSwxOCBRMzksMCA1NywxOCBRMzksMzYgMjEsMTgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iZ3JheSIvPjwvbWFya2VyPjwvZGVmcz48cGF0aCBzdHlsZT0ic3Ryb2tlOiBncmF5OyBmaWxsOiBub25lOyIgbWFya2VyLXN0YXJ0PSJ1cmwoI09OTFlfT05FX1NUQVJUKSIgbWFya2VyLWVuZD0idXJsKCNaRVJPX09SX01PUkVfRU5EKSIgZD0iTTEwMy43MDksMTQ5TDEwMy43MDksMTYwLjgzM0MxMDMuNzA5LDE3Mi42NjcsMTAzLjcwOSwxOTYuMzMzLDExNC40NSwyMTYuNUMxMjUuMTkxLDIzNi42NjcsMTQ2LjY3MiwyNTMuMzMzLDE1Ny40MTMsMjYxLjY2N0wxNjguMTU0LDI3MCIgY2xhc3M9ImVyIHJlbGF0aW9uc2hpcExpbmUiLz48cGF0aCBzdHlsZT0ic3Ryb2tlOiBncmF5OyBmaWxsOiBub25lOyIgbWFya2VyLXN0YXJ0PSJ1cmwoI09OTFlfT05FX1NUQVJUKSIgbWFya2VyLWVuZD0idXJsKCNaRVJPX09SX01PUkVfRU5EKSIgZD0iTTM3MS44MDEsMTcwTDM3MS44MDEsMTc4LjMzM0MzNzEuODAxLDE4Ni42NjcsMzcxLjgwMSwyMDMuMzMzLDM2MS4wNiwyMjBDMzUwLjMxOSwyMzYuNjY3LDMyOC44MzcsMjUzLjMzMywzMTguMDk2LDI2MS42NjdMMzA3LjM1NiwyNzAiIGNsYXNzPSJlciByZWxhdGlvbnNoaXBMaW5lIi8+PHBhdGggc3R5bGU9InN0cm9rZTogZ3JheTsgZmlsbDogbm9uZTsiIG1hcmtlci1zdGFydD0idXJsKCNPTkxZX09ORV9TVEFSVCkiIG1hcmtlci1lbmQ9InVybCgjWkVST19PUl9NT1JFX0VORCkiIGQ9Ik02MjkuMTM2LDE1OS41TDYyOS4xMzYsMTY5LjU4M0M2MjkuMTM2LDE3OS42NjcsNjI5LjEzNiwxOTkuODMzLDYzOC45OTIsMjE4LjI1QzY0OC44NDcsMjM2LjY2Nyw2NjguNTU5LDI1My4zMzMsNjc4LjQxNCwyNjEuNjY3TDY4OC4yNywyNzAiIGNsYXNzPSJlciByZWxhdGlvbnNoaXBMaW5lIi8+PHBhdGggc3R5bGU9InN0cm9rZTogZ3JheTsgZmlsbDogbm9uZTsiIG1hcmtlci1zdGFydD0idXJsKCNPTkxZX09ORV9TVEFSVCkiIG1hcmtlci1lbmQ9InVybCgjWkVST19PUl9NT1JFX0VORCkiIGQ9Ik04NzUuMTM1LDE3MEw4NzUuMTM1LDE3OC4zMzNDODc1LjEzNSwxODYuNjY3LDg3NS4xMzUsMjAzLjMzMyw4NjUuMjc5LDIyMEM4NTUuNDIzLDIzNi42NjcsODM1LjcxMiwyNTMuMzMzLDgyNS44NTYsMjYxLjY2N0w4MTYsMjcwIiBjbGFzcz0iZXIgcmVsYXRpb25zaGlwTGluZSIvPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIwLDQxICkiIGlkPSJlbnRpdHktYW5vbnltaXphdGlvbmRvY3VtZW50LThkYWFkMTFmLTBmYTUtNThiZi1iZWE4LWJiNjg3ZmViNTA4YSI+PHJlY3QgaGVpZ2h0PSIxMDgiIHdpZHRoPSIxNjcuNDE3OTY4NzUiIHk9IjAiIHg9IjAiIGNsYXNzPSJlciBlbnRpdHlCb3giLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgdGV4dC1hbmNob3I6IG1pZGRsZTsgZm9udC1zaXplOiAxMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoODMuNzA4OTg0Mzc1LDEyKSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudC04ZGFhZDExZi0wZmE1LTU4YmYtYmVhOC1iYjY4N2ZlYjUwOGEiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+YW5vbnltaXphdGlvbl9kb2N1bWVudDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjguMTIzNzU4OTUxODIyOTIiIHk9IjI0IiB4PSIwIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSwzNC41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudC04ZGFhZDExZi0wZmE1LTU4YmYtYmVhOC1iYjY4N2ZlYjUwOGEtYXR0ci0xLXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+VVVJRDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjguMzI4MDI4MzYxMDAyNjEiIHk9IjI0IiB4PSI2OC4xMjM3NTg5NTE4MjI5MiIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDczLjEyMzc1ODk1MTgyMjkyLDM0LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbmRvY3VtZW50LThkYWFkMTFmLTBmYTUtNThiZi1iZWE4LWJiNjg3ZmViNTA4YS1hdHRyLTEtbmFtZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5pZDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iMzAuOTY2MTgxNDM3MTc0NDgiIHk9IjI0IiB4PSIxMzYuNDUxNzg3MzEyODI1NTMiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNDEuNDUxNzg3MzEyODI1NTMsMzQuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9uZG9jdW1lbnQtOGRhYWQxMWYtMGZhNS01OGJmLWJlYTgtYmI2ODdmZWI1MDhhLWF0dHItMS1rZXkiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+UEs8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjY4LjEyMzc1ODk1MTgyMjkyIiB5PSI0NSIgeD0iMCIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1LDU1LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbmRvY3VtZW50LThkYWFkMTFmLTBmYTUtNThiZi1iZWE4LWJiNjg3ZmViNTA4YS1hdHRyLTItdHlwZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5EQVRFVElNRTwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjguMzI4MDI4MzYxMDAyNjEiIHk9IjQ1IiB4PSI2OC4xMjM3NTg5NTE4MjI5MiIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3My4xMjM3NTg5NTE4MjI5Miw1NS41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudC04ZGFhZDExZi0wZmE1LTU4YmYtYmVhOC1iYjY4N2ZlYjUwOGEtYXR0ci0yLW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+Y3JlYXRlZF9hdDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iMzAuOTY2MTgxNDM3MTc0NDgiIHk9IjQ1IiB4PSIxMzYuNDUxNzg3MzEyODI1NTMiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTQxLjQ1MTc4NzMxMjgyNTUzLDU1LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbmRvY3VtZW50LThkYWFkMTFmLTBmYTUtNThiZi1iZWE4LWJiNjg3ZmViNTA4YS1hdHRyLTIta2V5IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiLz48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjguMTIzNzU4OTUxODIyOTIiIHk9IjY2IiB4PSIwIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudC04ZGFhZDExZi0wZmE1LTU4YmYtYmVhOC1iYjY4N2ZlYjUwOGEtYXR0ci0zLXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+REFURVRJTUU8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjY4LjMyODAyODM2MTAwMjYxIiB5PSI2NiIgeD0iNjguMTIzNzU4OTUxODIyOTIiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3My4xMjM3NTg5NTE4MjI5Miw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudC04ZGFhZDExZi0wZmE1LTU4YmYtYmVhOC1iYjY4N2ZlYjUwOGEtYXR0ci0zLW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+dXBkYXRlZF9hdDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iMzAuOTY2MTgxNDM3MTc0NDgiIHk9IjY2IiB4PSIxMzYuNDUxNzg3MzEyODI1NTMiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNDEuNDUxNzg3MzEyODI1NTMsNzYuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9uZG9jdW1lbnQtOGRhYWQxMWYtMGZhNS01OGJmLWJlYTgtYmI2ODdmZWI1MDhhLWF0dHItMy1rZXkiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCIvPjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2OC4xMjM3NTg5NTE4MjI5MiIgeT0iODciIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw5Ny41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudC04ZGFhZDExZi0wZmE1LTU4YmYtYmVhOC1iYjY4N2ZlYjUwOGEtYXR0ci00LXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+VEVYVDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjguMzI4MDI4MzYxMDAyNjEiIHk9Ijg3IiB4PSI2OC4xMjM3NTg5NTE4MjI5MiIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3My4xMjM3NTg5NTE4MjI5Miw5Ny41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudC04ZGFhZDExZi0wZmE1LTU4YmYtYmVhOC1iYjY4N2ZlYjUwOGEtYXR0ci00LW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+bmFtZTwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iMzAuOTY2MTgxNDM3MTc0NDgiIHk9Ijg3IiB4PSIxMzYuNDUxNzg3MzEyODI1NTMiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTQxLjQ1MTc4NzMxMjgyNTUzLDk3LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbmRvY3VtZW50LThkYWFkMTFmLTBmYTUtNThiZi1iZWE4LWJiNjg3ZmViNTA4YS1hdHRyLTQta2V5IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiLz48L2c+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjg3LjQxNzk2ODc1LDIwICkiIGlkPSJlbnRpdHktYW5vbnltaXphdGlvbnBhcmFncmFwaC1mNWM2NTM5My01MDYwLTU0MzItYjM1Ny04ZjkzNWE1OGExZTMiPjxyZWN0IGhlaWdodD0iMTUwIiB3aWR0aD0iMTY4Ljc2NTYyNSIgeT0iMCIgeD0iMCIgY2xhc3M9ImVyIGVudGl0eUJveCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyB0ZXh0LWFuY2hvcjogbWlkZGxlOyBmb250LXNpemU6IDEycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg4NC4zODI4MTI1LDEyKSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25wYXJhZ3JhcGgtZjVjNjUzOTMtNTA2MC01NDMyLWIzNTctOGY5MzVhNThhMWUzIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPmFub255bWl6YXRpb25fcGFyYWdyYXBoPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2OC41NzI5Nzc3MDE4MjI5MiIgeT0iMjQiIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1LDM0LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbnBhcmFncmFwaC1mNWM2NTM5My01MDYwLTU0MzItYjM1Ny04ZjkzNWE1OGExZTMtYXR0ci0xLXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+VVVJRDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjguNzc3MjQ3MTExMDAyNjEiIHk9IjI0IiB4PSI2OC41NzI5Nzc3MDE4MjI5MiIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDczLjU3Mjk3NzcwMTgyMjkyLDM0LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbnBhcmFncmFwaC1mNWM2NTM5My01MDYwLTU0MzItYjM1Ny04ZjkzNWE1OGExZTMtYXR0ci0xLW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+aWQ8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjMxLjQxNTQwMDE4NzE3NDQ4IiB5PSIyNCIgeD0iMTM3LjM1MDIyNDgxMjgyNTUzIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTQyLjM1MDIyNDgxMjgyNTUzLDM0LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbnBhcmFncmFwaC1mNWM2NTM5My01MDYwLTU0MzItYjM1Ny04ZjkzNWE1OGExZTMtYXR0ci0xLWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5QSzwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjguNTcyOTc3NzAxODIyOTIiIHk9IjQ1IiB4PSIwIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUsNTUuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9ucGFyYWdyYXBoLWY1YzY1MzkzLTUwNjAtNTQzMi1iMzU3LThmOTM1YTU4YTFlMy1hdHRyLTItdHlwZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5URVhUPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2OC43NzcyNDcxMTEwMDI2MSIgeT0iNDUiIHg9IjY4LjU3Mjk3NzcwMTgyMjkyIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDczLjU3Mjk3NzcwMTgyMjkyLDU1LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbnBhcmFncmFwaC1mNWM2NTM5My01MDYwLTU0MzItYjM1Ny04ZjkzNWE1OGExZTMtYXR0ci0yLW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+dGV4dDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iMzEuNDE1NDAwMTg3MTc0NDgiIHk9IjQ1IiB4PSIxMzcuMzUwMjI0ODEyODI1NTMiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTQyLjM1MDIyNDgxMjgyNTUzLDU1LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbnBhcmFncmFwaC1mNWM2NTM5My01MDYwLTU0MzItYjM1Ny04ZjkzNWE1OGExZTMtYXR0ci0yLWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIi8+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjY4LjU3Mjk3NzcwMTgyMjkyIiB5PSI2NiIgeD0iMCIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUsNzYuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9ucGFyYWdyYXBoLWY1YzY1MzkzLTUwNjAtNTQzMi1iMzU3LThmOTM1YTU4YTFlMy1hdHRyLTMtdHlwZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5KU09OPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2OC43NzcyNDcxMTEwMDI2MSIgeT0iNjYiIHg9IjY4LjU3Mjk3NzcwMTgyMjkyIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNzMuNTcyOTc3NzAxODIyOTIsNzYuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9ucGFyYWdyYXBoLWY1YzY1MzkzLTUwNjAtNTQzMi1iMzU3LThmOTM1YTU4YTFlMy1hdHRyLTMtbmFtZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5wcmVkaWN0aW9uPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSIzMS40MTU0MDAxODcxNzQ0OCIgeT0iNjYiIHg9IjEzNy4zNTAyMjQ4MTI4MjU1MyIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE0Mi4zNTAyMjQ4MTI4MjU1Myw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25wYXJhZ3JhcGgtZjVjNjUzOTMtNTA2MC01NDMyLWIzNTctOGY5MzVhNThhMWUzLWF0dHItMy1rZXkiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCIvPjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2OC41NzI5Nzc3MDE4MjI5MiIgeT0iODciIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw5Ny41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25wYXJhZ3JhcGgtZjVjNjUzOTMtNTA2MC01NDMyLWIzNTctOGY5MzVhNThhMWUzLWF0dHItNC10eXBlIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPkpTT048L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjY4Ljc3NzI0NzExMTAwMjYxIiB5PSI4NyIgeD0iNjguNTcyOTc3NzAxODIyOTIiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNzMuNTcyOTc3NzAxODIyOTIsOTcuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9ucGFyYWdyYXBoLWY1YzY1MzkzLTUwNjAtNTQzMi1iMzU3LThmOTM1YTU4YTFlMy1hdHRyLTQtbmFtZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj52YWxpZGF0aW9uPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSIzMS40MTU0MDAxODcxNzQ0OCIgeT0iODciIHg9IjEzNy4zNTAyMjQ4MTI4MjU1MyIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNDIuMzUwMjI0ODEyODI1NTMsOTcuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9ucGFyYWdyYXBoLWY1YzY1MzkzLTUwNjAtNTQzMi1iMzU3LThmOTM1YTU4YTFlMy1hdHRyLTQta2V5IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiLz48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjguNTcyOTc3NzAxODIyOTIiIHk9IjEwOCIgeD0iMCIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUsMTE4LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbnBhcmFncmFwaC1mNWM2NTM5My01MDYwLTU0MzItYjM1Ny04ZjkzNWE1OGExZTMtYXR0ci01LXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+REFURVRJTUU8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjY4Ljc3NzI0NzExMTAwMjYxIiB5PSIxMDgiIHg9IjY4LjU3Mjk3NzcwMTgyMjkyIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNzMuNTcyOTc3NzAxODIyOTIsMTE4LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbnBhcmFncmFwaC1mNWM2NTM5My01MDYwLTU0MzItYjM1Ny04ZjkzNWE1OGExZTMtYXR0ci01LW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+Y3JlYXRlZF9hdDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iMzEuNDE1NDAwMTg3MTc0NDgiIHk9IjEwOCIgeD0iMTM3LjM1MDIyNDgxMjgyNTUzIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTQyLjM1MDIyNDgxMjgyNTUzLDExOC41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25wYXJhZ3JhcGgtZjVjNjUzOTMtNTA2MC01NDMyLWIzNTctOGY5MzVhNThhMWUzLWF0dHItNS1rZXkiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCIvPjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2OC41NzI5Nzc3MDE4MjI5MiIgeT0iMTI5IiB4PSIwIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUsMTM5LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbnBhcmFncmFwaC1mNWM2NTM5My01MDYwLTU0MzItYjM1Ny04ZjkzNWE1OGExZTMtYXR0ci02LXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+REFURVRJTUU8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjY4Ljc3NzI0NzExMTAwMjYxIiB5PSIxMjkiIHg9IjY4LjU3Mjk3NzcwMTgyMjkyIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDczLjU3Mjk3NzcwMTgyMjkyLDEzOS41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25wYXJhZ3JhcGgtZjVjNjUzOTMtNTA2MC01NDMyLWIzNTctOGY5MzVhNThhMWUzLWF0dHItNi1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPnVwZGF0ZWRfYXQ8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjMxLjQxNTQwMDE4NzE3NDQ4IiB5PSIxMjkiIHg9IjEzNy4zNTAyMjQ4MTI4MjU1MyIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNDIuMzUwMjI0ODEyODI1NTMsMTM5LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbnBhcmFncmFwaC1mNWM2NTM5My01MDYwLTU0MzItYjM1Ny04ZjkzNWE1OGExZTMtYXR0ci02LWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIi8+PC9nPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyMy42ODQ1NzAzMTI1LDI3MCApIiBpZD0iZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudHBhcmFncmFwaC05MGMwMzBmMC1mNzYzLTU3NWUtOGU3Ni0yNmU1M2Y4YjRmYzkiPjxyZWN0IGhlaWdodD0iMTA4IiB3aWR0aD0iMjI4LjE0MDYyNSIgeT0iMCIgeD0iMCIgY2xhc3M9ImVyIGVudGl0eUJveCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyB0ZXh0LWFuY2hvcjogbWlkZGxlOyBmb250LXNpemU6IDEycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMTQuMDcwMzEyNSwxMikiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9uZG9jdW1lbnRwYXJhZ3JhcGgtOTBjMDMwZjAtZjc2My01NzVlLThlNzYtMjZlNTNmOGI0ZmM5IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPmFub255bWl6YXRpb25fZG9jdW1lbnRfcGFyYWdyYXBoPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI3Ni4yMzg3NTkzNTg3MjM5NSIgeT0iMjQiIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1LDM0LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbmRvY3VtZW50cGFyYWdyYXBoLTkwYzAzMGYwLWY3NjMtNTc1ZS04ZTc2LTI2ZTUzZjhiNGZjOS1hdHRyLTEtdHlwZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5VVUlEPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI5MS4zMTY2NzA3MzU2NzcwOCIgeT0iMjQiIHg9Ijc2LjIzODc1OTM1ODcyMzk1IiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoODEuMjM4NzU5MzU4NzIzOTUsMzQuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9uZG9jdW1lbnRwYXJhZ3JhcGgtOTBjMDMwZjAtZjc2My01NzVlLThlNzYtMjZlNTNmOGI0ZmM5LWF0dHItMS1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPmlkPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2MC41ODUxOTQ5MDU1OTg5NTQiIHk9IjI0IiB4PSIxNjcuNTU1NDMwMDk0NDAxMDMiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNzIuNTU1NDMwMDk0NDAxMDMsMzQuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9uZG9jdW1lbnRwYXJhZ3JhcGgtOTBjMDMwZjAtZjc2My01NzVlLThlNzYtMjZlNTNmOGI0ZmM5LWF0dHItMS1rZXkiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+UEs8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9Ijc2LjIzODc1OTM1ODcyMzk1IiB5PSI0NSIgeD0iMCIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1LDU1LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbmRvY3VtZW50cGFyYWdyYXBoLTkwYzAzMGYwLWY3NjMtNTc1ZS04ZTc2LTI2ZTUzZjhiNGZjOS1hdHRyLTItdHlwZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5VVUlEPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI5MS4zMTY2NzA3MzU2NzcwOCIgeT0iNDUiIHg9Ijc2LjIzODc1OTM1ODcyMzk1IiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDgxLjIzODc1OTM1ODcyMzk1LDU1LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbmRvY3VtZW50cGFyYWdyYXBoLTkwYzAzMGYwLWY3NjMtNTc1ZS04ZTc2LTI2ZTUzZjhiNGZjOS1hdHRyLTItbmFtZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5kb2N1bWVudF9pZDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjAuNTg1MTk0OTA1NTk4OTU0IiB5PSI0NSIgeD0iMTY3LjU1NTQzMDA5NDQwMTAzIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE3Mi41NTU0MzAwOTQ0MDEwMyw1NS41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudHBhcmFncmFwaC05MGMwMzBmMC1mNzYzLTU3NWUtOGU3Ni0yNmU1M2Y4YjRmYzktYXR0ci0yLWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5QSyxGSzwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNzYuMjM4NzU5MzU4NzIzOTUiIHk9IjY2IiB4PSIwIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudHBhcmFncmFwaC05MGMwMzBmMC1mNzYzLTU3NWUtOGU3Ni0yNmU1M2Y4YjRmYzktYXR0ci0zLXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+VVVJRDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iOTEuMzE2NjcwNzM1Njc3MDgiIHk9IjY2IiB4PSI3Ni4yMzg3NTkzNTg3MjM5NSIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDgxLjIzODc1OTM1ODcyMzk1LDc2LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbmRvY3VtZW50cGFyYWdyYXBoLTkwYzAzMGYwLWY3NjMtNTc1ZS04ZTc2LTI2ZTUzZjhiNGZjOS1hdHRyLTMtbmFtZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5wYXJhZ3JhcGhfaWQ8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjYwLjU4NTE5NDkwNTU5ODk1NCIgeT0iNjYiIHg9IjE2Ny41NTU0MzAwOTQ0MDEwMyIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE3Mi41NTU0MzAwOTQ0MDEwMyw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudHBhcmFncmFwaC05MGMwMzBmMC1mNzYzLTU3NWUtOGU3Ni0yNmU1M2Y4YjRmYzktYXR0ci0zLWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5QSyxGSzwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNzYuMjM4NzU5MzU4NzIzOTUiIHk9Ijg3IiB4PSIwIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUsOTcuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9uZG9jdW1lbnRwYXJhZ3JhcGgtOTBjMDMwZjAtZjc2My01NzVlLThlNzYtMjZlNTNmOGI0ZmM5LWF0dHItNC10eXBlIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPklOVEVHRVI8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjkxLjMxNjY3MDczNTY3NzA4IiB5PSI4NyIgeD0iNzYuMjM4NzU5MzU4NzIzOTUiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoODEuMjM4NzU5MzU4NzIzOTUsOTcuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9uZG9jdW1lbnRwYXJhZ3JhcGgtOTBjMDMwZjAtZjc2My01NzVlLThlNzYtMjZlNTNmOGI0ZmM5LWF0dHItNC1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPm9yZGVyPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2MC41ODUxOTQ5MDU1OTg5NTQiIHk9Ijg3IiB4PSIxNjcuNTU1NDMwMDk0NDAxMDMiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTcyLjU1NTQzMDA5NDQwMTAzLDk3LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbmRvY3VtZW50cGFyYWdyYXBoLTkwYzAzMGYwLWY3NjMtNTc1ZS04ZTc2LTI2ZTUzZjhiNGZjOS1hdHRyLTQta2V5IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiLz48L2c+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNTU2LjE4MzU5Mzc1LDMwLjUgKSIgaWQ9ImVudGl0eS1kYXRhcHVibGljZG9jdW1lbnQtYTBiZTViOTQtOTNkNi01MTZiLTg1YWEtNTliNmM2ZTRjODY1Ij48cmVjdCBoZWlnaHQ9IjEyOSIgd2lkdGg9IjE0NS45MDQzNzMxNjg5NDUzIiB5PSIwIiB4PSIwIiBjbGFzcz0iZXIgZW50aXR5Qm94Ii8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IHRleHQtYW5jaG9yOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDcyLjk1MjE4NjU4NDQ3MjY2LDEyKSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudC1hMGJlNWI5NC05M2Q2LTUxNmItODVhYS01OWI2YzZlNGM4NjUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+ZGF0YXB1YmxpY19kb2N1bWVudDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjAuOTUyNTYwNDI0ODA0NjkiIHk9IjI0IiB4PSIwIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSwzNC41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudC1hMGJlNWI5NC05M2Q2LTUxNmItODVhYS01OWI2YzZlNGM4NjUtYXR0ci0xLXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+VVVJRDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjEuMTU2ODI5ODMzOTg0Mzc1IiB5PSIyNCIgeD0iNjAuOTUyNTYwNDI0ODA0NjkiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2NS45NTI1NjA0MjQ4MDQ2OSwzNC41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudC1hMGJlNWI5NC05M2Q2LTUxNmItODVhYS01OWI2YzZlNGM4NjUtYXR0ci0xLW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+aWQ8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjIzLjc5NDk4MjkxMDE1NjI1IiB5PSIyNCIgeD0iMTIyLjEwOTM5MDI1ODc4OTA2IiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTI3LjEwOTM5MDI1ODc4OTA2LDM0LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY2RvY3VtZW50LWEwYmU1Yjk0LTkzZDYtNTE2Yi04NWFhLTU5YjZjNmU0Yzg2NS1hdHRyLTEta2V5IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPlBLPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2MC45NTI1NjA0MjQ4MDQ2OSIgeT0iNDUiIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw1NS41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudC1hMGJlNWI5NC05M2Q2LTUxNmItODVhYS01OWI2YzZlNGM4NjUtYXR0ci0yLXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+SlNPTjwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjEuMTU2ODI5ODMzOTg0Mzc1IiB5PSI0NSIgeD0iNjAuOTUyNTYwNDI0ODA0NjkiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjUuOTUyNTYwNDI0ODA0NjksNTUuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnQtYTBiZTViOTQtOTNkNi01MTZiLTg1YWEtNTliNmM2ZTRjODY1LWF0dHItMi1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPnByZWRpY3Rpb248L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjIzLjc5NDk4MjkxMDE1NjI1IiB5PSI0NSIgeD0iMTIyLjEwOTM5MDI1ODc4OTA2IiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyNy4xMDkzOTAyNTg3ODkwNiw1NS41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudC1hMGJlNWI5NC05M2Q2LTUxNmItODVhYS01OWI2YzZlNGM4NjUtYXR0ci0yLWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIi8+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjYwLjk1MjU2MDQyNDgwNDY5IiB5PSI2NiIgeD0iMCIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUsNzYuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnQtYTBiZTViOTQtOTNkNi01MTZiLTg1YWEtNTliNmM2ZTRjODY1LWF0dHItMy10eXBlIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPkpTT048L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjYxLjE1NjgyOTgzMzk4NDM3NSIgeT0iNjYiIHg9IjYwLjk1MjU2MDQyNDgwNDY5IiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjUuOTUyNTYwNDI0ODA0NjksNzYuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnQtYTBiZTViOTQtOTNkNi01MTZiLTg1YWEtNTliNmM2ZTRjODY1LWF0dHItMy1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPnZhbGlkYXRpb248L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjIzLjc5NDk4MjkxMDE1NjI1IiB5PSI2NiIgeD0iMTIyLjEwOTM5MDI1ODc4OTA2IiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTI3LjEwOTM5MDI1ODc4OTA2LDc2LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY2RvY3VtZW50LWEwYmU1Yjk0LTkzZDYtNTE2Yi04NWFhLTU5YjZjNmU0Yzg2NS1hdHRyLTMta2V5IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiLz48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjAuOTUyNTYwNDI0ODA0NjkiIHk9Ijg3IiB4PSIwIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUsOTcuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnQtYTBiZTViOTQtOTNkNi01MTZiLTg1YWEtNTliNmM2ZTRjODY1LWF0dHItNC10eXBlIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPkRBVEVUSU1FPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2MS4xNTY4Mjk4MzM5ODQzNzUiIHk9Ijg3IiB4PSI2MC45NTI1NjA0MjQ4MDQ2OSIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2NS45NTI1NjA0MjQ4MDQ2OSw5Ny41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudC1hMGJlNWI5NC05M2Q2LTUxNmItODVhYS01OWI2YzZlNGM4NjUtYXR0ci00LW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+Y3JlYXRlZF9hdDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iMjMuNzk0OTgyOTEwMTU2MjUiIHk9Ijg3IiB4PSIxMjIuMTA5MzkwMjU4Nzg5MDYiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTI3LjEwOTM5MDI1ODc4OTA2LDk3LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY2RvY3VtZW50LWEwYmU1Yjk0LTkzZDYtNTE2Yi04NWFhLTU5YjZjNmU0Yzg2NS1hdHRyLTQta2V5IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiLz48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjAuOTUyNTYwNDI0ODA0NjkiIHk9IjEwOCIgeD0iMCIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUsMTE4LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY2RvY3VtZW50LWEwYmU1Yjk0LTkzZDYtNTE2Yi04NWFhLTU5YjZjNmU0Yzg2NS1hdHRyLTUtdHlwZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5EQVRFVElNRTwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjEuMTU2ODI5ODMzOTg0Mzc1IiB5PSIxMDgiIHg9IjYwLjk1MjU2MDQyNDgwNDY5IiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjUuOTUyNTYwNDI0ODA0NjksMTE4LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY2RvY3VtZW50LWEwYmU1Yjk0LTkzZDYtNTE2Yi04NWFhLTU5YjZjNmU0Yzg2NS1hdHRyLTUtbmFtZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj51cGRhdGVkX2F0PC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSIyMy43OTQ5ODI5MTAxNTYyNSIgeT0iMTA4IiB4PSIxMjIuMTA5MzkwMjU4Nzg5MDYiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMjcuMTA5MzkwMjU4Nzg5MDYsMTE4LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY2RvY3VtZW50LWEwYmU1Yjk0LTkzZDYtNTE2Yi04NWFhLTU5YjZjNmU0Yzg2NS1hdHRyLTUta2V5IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiLz48L2c+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoODAyLjA4Nzk2NjkxODk0NTMsMjAgKSIgaWQ9ImVudGl0eS1kYXRhcHVibGljcGFyYWdyYXBoLWE3YTE2NDVlLWEzZTctNTIzNy04M2NlLTMxYTVkNzUwNjg4YiI+PHJlY3QgaGVpZ2h0PSIxNTAiIHdpZHRoPSIxNDYuMDkzNzUiIHk9IjAiIHg9IjAiIGNsYXNzPSJlciBlbnRpdHlCb3giLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgdGV4dC1hbmNob3I6IG1pZGRsZTsgZm9udC1zaXplOiAxMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNzMuMDQ2ODc1LDEyKSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNwYXJhZ3JhcGgtYTdhMTY0NWUtYTNlNy01MjM3LTgzY2UtMzFhNWQ3NTA2ODhiIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPmRhdGFwdWJsaWNfcGFyYWdyYXBoPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2MS4wMTU2ODYwMzUxNTYyNSIgeT0iMjQiIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1LDM0LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY3BhcmFncmFwaC1hN2ExNjQ1ZS1hM2U3LTUyMzctODNjZS0zMWE1ZDc1MDY4OGItYXR0ci0xLXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+VVVJRDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjEuMjE5OTU1NDQ0MzM1OTQiIHk9IjI0IiB4PSI2MS4wMTU2ODYwMzUxNTYyNSIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDY2LjAxNTY4NjAzNTE1NjI1LDM0LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY3BhcmFncmFwaC1hN2ExNjQ1ZS1hM2U3LTUyMzctODNjZS0zMWE1ZDc1MDY4OGItYXR0ci0xLW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+aWQ8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjIzLjg1ODEwODUyMDUwNzgxMiIgeT0iMjQiIHg9IjEyMi4yMzU2NDE0Nzk0OTIxOSIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyNy4yMzU2NDE0Nzk0OTIxOSwzNC41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNwYXJhZ3JhcGgtYTdhMTY0NWUtYTNlNy01MjM3LTgzY2UtMzFhNWQ3NTA2ODhiLWF0dHItMS1rZXkiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+UEs8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjYxLjAxNTY4NjAzNTE1NjI1IiB5PSI0NSIgeD0iMCIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1LDU1LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY3BhcmFncmFwaC1hN2ExNjQ1ZS1hM2U3LTUyMzctODNjZS0zMWE1ZDc1MDY4OGItYXR0ci0yLXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+VEVYVDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjEuMjE5OTU1NDQ0MzM1OTQiIHk9IjQ1IiB4PSI2MS4wMTU2ODYwMzUxNTYyNSIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2Ni4wMTU2ODYwMzUxNTYyNSw1NS41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNwYXJhZ3JhcGgtYTdhMTY0NWUtYTNlNy01MjM3LTgzY2UtMzFhNWQ3NTA2ODhiLWF0dHItMi1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPnRleHQ8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjIzLjg1ODEwODUyMDUwNzgxMiIgeT0iNDUiIHg9IjEyMi4yMzU2NDE0Nzk0OTIxOSIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMjcuMjM1NjQxNDc5NDkyMTksNTUuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljcGFyYWdyYXBoLWE3YTE2NDVlLWEzZTctNTIzNy04M2NlLTMxYTVkNzUwNjg4Yi1hdHRyLTIta2V5IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiLz48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjEuMDE1Njg2MDM1MTU2MjUiIHk9IjY2IiB4PSIwIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNwYXJhZ3JhcGgtYTdhMTY0NWUtYTNlNy01MjM3LTgzY2UtMzFhNWQ3NTA2ODhiLWF0dHItMy10eXBlIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPkpTT048L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjYxLjIxOTk1NTQ0NDMzNTk0IiB5PSI2NiIgeD0iNjEuMDE1Njg2MDM1MTU2MjUiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2Ni4wMTU2ODYwMzUxNTYyNSw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNwYXJhZ3JhcGgtYTdhMTY0NWUtYTNlNy01MjM3LTgzY2UtMzFhNWQ3NTA2ODhiLWF0dHItMy1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPnByZWRpY3Rpb248L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjIzLjg1ODEwODUyMDUwNzgxMiIgeT0iNjYiIHg9IjEyMi4yMzU2NDE0Nzk0OTIxOSIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyNy4yMzU2NDE0Nzk0OTIxOSw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNwYXJhZ3JhcGgtYTdhMTY0NWUtYTNlNy01MjM3LTgzY2UtMzFhNWQ3NTA2ODhiLWF0dHItMy1rZXkiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCIvPjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2MS4wMTU2ODYwMzUxNTYyNSIgeT0iODciIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw5Ny41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNwYXJhZ3JhcGgtYTdhMTY0NWUtYTNlNy01MjM3LTgzY2UtMzFhNWQ3NTA2ODhiLWF0dHItNC10eXBlIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPkpTT048L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjYxLjIxOTk1NTQ0NDMzNTk0IiB5PSI4NyIgeD0iNjEuMDE1Njg2MDM1MTU2MjUiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjYuMDE1Njg2MDM1MTU2MjUsOTcuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljcGFyYWdyYXBoLWE3YTE2NDVlLWEzZTctNTIzNy04M2NlLTMxYTVkNzUwNjg4Yi1hdHRyLTQtbmFtZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj52YWxpZGF0aW9uPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSIyMy44NTgxMDg1MjA1MDc4MTIiIHk9Ijg3IiB4PSIxMjIuMjM1NjQxNDc5NDkyMTkiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTI3LjIzNTY0MTQ3OTQ5MjE5LDk3LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY3BhcmFncmFwaC1hN2ExNjQ1ZS1hM2U3LTUyMzctODNjZS0zMWE1ZDc1MDY4OGItYXR0ci00LWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIi8+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjYxLjAxNTY4NjAzNTE1NjI1IiB5PSIxMDgiIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1LDExOC41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNwYXJhZ3JhcGgtYTdhMTY0NWUtYTNlNy01MjM3LTgzY2UtMzFhNWQ3NTA2ODhiLWF0dHItNS10eXBlIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPkRBVEVUSU1FPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2MS4yMTk5NTU0NDQzMzU5NCIgeT0iMTA4IiB4PSI2MS4wMTU2ODYwMzUxNTYyNSIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDY2LjAxNTY4NjAzNTE1NjI1LDExOC41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNwYXJhZ3JhcGgtYTdhMTY0NWUtYTNlNy01MjM3LTgzY2UtMzFhNWQ3NTA2ODhiLWF0dHItNS1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPmNyZWF0ZWRfYXQ8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjIzLjg1ODEwODUyMDUwNzgxMiIgeT0iMTA4IiB4PSIxMjIuMjM1NjQxNDc5NDkyMTkiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMjcuMjM1NjQxNDc5NDkyMTksMTE4LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY3BhcmFncmFwaC1hN2ExNjQ1ZS1hM2U3LTUyMzctODNjZS0zMWE1ZDc1MDY4OGItYXR0ci01LWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIi8+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjYxLjAxNTY4NjAzNTE1NjI1IiB5PSIxMjkiIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSwxMzkuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljcGFyYWdyYXBoLWE3YTE2NDVlLWEzZTctNTIzNy04M2NlLTMxYTVkNzUwNjg4Yi1hdHRyLTYtdHlwZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5EQVRFVElNRTwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjEuMjE5OTU1NDQ0MzM1OTQiIHk9IjEyOSIgeD0iNjEuMDE1Njg2MDM1MTU2MjUiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjYuMDE1Njg2MDM1MTU2MjUsMTM5LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY3BhcmFncmFwaC1hN2ExNjQ1ZS1hM2U3LTUyMzctODNjZS0zMWE1ZDc1MDY4OGItYXR0ci02LW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+dXBkYXRlZF9hdDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iMjMuODU4MTA4NTIwNTA3ODEyIiB5PSIxMjkiIHg9IjEyMi4yMzU2NDE0Nzk0OTIxOSIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMjcuMjM1NjQxNDc5NDkyMTksMTM5LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY3BhcmFncmFwaC1hN2ExNjQ1ZS1hM2U3LTUyMzctODNjZS0zMWE1ZDc1MDY4OGItYXR0ci02LWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIi8+PC9nPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDY0OS40MDA5MzYxMjY3MDksMjcwICkiIGlkPSJlbnRpdHktZGF0YXB1YmxpY2RvY3VtZW50cGFyYWdyYXBoLWVmMGYzODg3LTJkYmEtNTc2ZS04NjM0LTg1NjI3OGI2OGI0NiI+PHJlY3QgaGVpZ2h0PSIxMDgiIHdpZHRoPSIyMDUuNDY4NzUiIHk9IjAiIHg9IjAiIGNsYXNzPSJlciBlbnRpdHlCb3giLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgdGV4dC1hbmNob3I6IG1pZGRsZTsgZm9udC1zaXplOiAxMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAyLjczNDM3NSwxMikiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnRwYXJhZ3JhcGgtZWYwZjM4ODctMmRiYS01NzZlLTg2MzQtODU2Mjc4YjY4YjQ2IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPmRhdGFwdWJsaWNfZG9jdW1lbnRfcGFyYWdyYXBoPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2OC42ODE0Njc2OTIwNTczIiB5PSIyNCIgeD0iMCIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUsMzQuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnRwYXJhZ3JhcGgtZWYwZjM4ODctMmRiYS01NzZlLTg2MzQtODU2Mjc4YjY4YjQ2LWF0dHItMS10eXBlIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPlVVSUQ8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjgzLjc1OTM3OTA2OTAxMDQyIiB5PSIyNCIgeD0iNjguNjgxNDY3NjkyMDU3MyIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDczLjY4MTQ2NzY5MjA1NzMsMzQuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnRwYXJhZ3JhcGgtZWYwZjM4ODctMmRiYS01NzZlLTg2MzQtODU2Mjc4YjY4YjQ2LWF0dHItMS1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPmlkPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI1My4wMjc5MDMyMzg5MzIyOSIgeT0iMjQiIHg9IjE1Mi40NDA4NDY3NjEwNjc3MiIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE1Ny40NDA4NDY3NjEwNjc3MiwzNC41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudHBhcmFncmFwaC1lZjBmMzg4Ny0yZGJhLTU3NmUtODYzNC04NTYyNzhiNjhiNDYtYXR0ci0xLWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5QSzwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjguNjgxNDY3NjkyMDU3MyIgeT0iNDUiIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw1NS41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudHBhcmFncmFwaC1lZjBmMzg4Ny0yZGJhLTU3NmUtODYzNC04NTYyNzhiNjhiNDYtYXR0ci0yLXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+VVVJRDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iODMuNzU5Mzc5MDY5MDEwNDIiIHk9IjQ1IiB4PSI2OC42ODE0Njc2OTIwNTczIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDczLjY4MTQ2NzY5MjA1NzMsNTUuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnRwYXJhZ3JhcGgtZWYwZjM4ODctMmRiYS01NzZlLTg2MzQtODU2Mjc4YjY4YjQ2LWF0dHItMi1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPmRvY3VtZW50X2lkPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI1My4wMjc5MDMyMzg5MzIyOSIgeT0iNDUiIHg9IjE1Mi40NDA4NDY3NjEwNjc3MiIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNTcuNDQwODQ2NzYxMDY3NzIsNTUuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnRwYXJhZ3JhcGgtZWYwZjM4ODctMmRiYS01NzZlLTg2MzQtODU2Mjc4YjY4YjQ2LWF0dHItMi1rZXkiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+UEssRks8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjY4LjY4MTQ2NzY5MjA1NzMiIHk9IjY2IiB4PSIwIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudHBhcmFncmFwaC1lZjBmMzg4Ny0yZGJhLTU3NmUtODYzNC04NTYyNzhiNjhiNDYtYXR0ci0zLXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+VVVJRDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iODMuNzU5Mzc5MDY5MDEwNDIiIHk9IjY2IiB4PSI2OC42ODE0Njc2OTIwNTczIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNzMuNjgxNDY3NjkyMDU3Myw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudHBhcmFncmFwaC1lZjBmMzg4Ny0yZGJhLTU3NmUtODYzNC04NTYyNzhiNjhiNDYtYXR0ci0zLW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+cGFyYWdyYXBoX2lkPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI1My4wMjc5MDMyMzg5MzIyOSIgeT0iNjYiIHg9IjE1Mi40NDA4NDY3NjEwNjc3MiIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE1Ny40NDA4NDY3NjEwNjc3Miw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudHBhcmFncmFwaC1lZjBmMzg4Ny0yZGJhLTU3NmUtODYzNC04NTYyNzhiNjhiNDYtYXR0ci0zLWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5QSyxGSzwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjguNjgxNDY3NjkyMDU3MyIgeT0iODciIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw5Ny41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudHBhcmFncmFwaC1lZjBmMzg4Ny0yZGJhLTU3NmUtODYzNC04NTYyNzhiNjhiNDYtYXR0ci00LXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+SU5URUdFUjwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iODMuNzU5Mzc5MDY5MDEwNDIiIHk9Ijg3IiB4PSI2OC42ODE0Njc2OTIwNTczIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDczLjY4MTQ2NzY5MjA1NzMsOTcuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnRwYXJhZ3JhcGgtZWYwZjM4ODctMmRiYS01NzZlLTg2MzQtODU2Mjc4YjY4YjQ2LWF0dHItNC1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPm9yZGVyPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI1My4wMjc5MDMyMzg5MzIyOSIgeT0iODciIHg9IjE1Mi40NDA4NDY3NjEwNjc3MiIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNTcuNDQwODQ2NzYxMDY3NzIsOTcuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnRwYXJhZ3JhcGgtZWYwZjM4ODctMmRiYS01NzZlLTg2MzQtODU2Mjc4YjY4YjQ2LWF0dHItNC1rZXkiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCIvPjwvZz48cmVjdCBoZWlnaHQ9IjE0IiB3aWR0aD0iMjQuMDE1NjI1IiB5PSIyMTIuNjYzMDQwMTYxMTMyOCIgeD0iMTA0LjIyOTUwNzQ0NjI4OTA2IiBjbGFzcz0iZXIgcmVsYXRpb25zaGlwTGFiZWxCb3giLz48dGV4dCBzdHlsZT0idGV4dC1hbmNob3I6IG1pZGRsZTsgZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMnB4OyIgeT0iMjE5LjY2MzA0MDE2MTEzMjgiIHg9IjExNi4yMzczMTk5NDYyODkwNiIgaWQ9InJlbDI1IiBjbGFzcz0iZXIgcmVsYXRpb25zaGlwTGFiZWwiPmxpbmtzPC90ZXh0PjxyZWN0IGhlaWdodD0iMTQiIHdpZHRoPSIyNC4wMTU2MjUiIHk9IjIyMS42NzgwNzAwNjgzNTkzOCIgeD0iMzQyLjU2NDE3ODQ2Njc5NjkiIGNsYXNzPSJlciByZWxhdGlvbnNoaXBMYWJlbEJveCIvPjx0ZXh0IHN0eWxlPSJ0ZXh0LWFuY2hvcjogbWlkZGxlOyBkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEycHg7IiB5PSIyMjguNjc4MDcwMDY4MzU5MzgiIHg9IjM1NC41NzE5OTA5NjY3OTY5IiBpZD0icmVsMjYiIGNsYXNzPSJlciByZWxhdGlvbnNoaXBMYWJlbCI+bGlua3M8L3RleHQ+PHJlY3QgaGVpZ2h0PSIxNCIgd2lkdGg9IjI0LjAxNTYyNSIgeT0iMjE2LjI1NzAxOTA0Mjk2ODc1IiB4PSI2MjkuOTIyNjA3NDIxODc1IiBjbGFzcz0iZXIgcmVsYXRpb25zaGlwTGFiZWxCb3giLz48dGV4dCBzdHlsZT0idGV4dC1hbmNob3I6IG1pZGRsZTsgZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMnB4OyIgeT0iMjIzLjI1NzAxOTA0Mjk2ODc1IiB4PSI2NDEuOTMwNDE5OTIxODc1IiBpZD0icmVsMjciIGNsYXNzPSJlciByZWxhdGlvbnNoaXBMYWJlbCI+bGlua3M8L3RleHQ+PHJlY3QgaGVpZ2h0PSIxNCIgd2lkdGg9IjI0LjAxNTYyNSIgeT0iMjIwLjc4MTM1NjgxMTUyMzQ0IiB4PSI4NDguMDA0MDg5MzU1NDY4OCIgY2xhc3M9ImVyIHJlbGF0aW9uc2hpcExhYmVsQm94Ii8+PHRleHQgc3R5bGU9InRleHQtYW5jaG9yOiBtaWRkbGU7IGRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTJweDsiIHk9IjIyNy43ODEzNTY4MTE1MjM0NCIgeD0iODYwLjAxMTkwMTg1NTQ2ODgiIGlkPSJyZWwyOCIgY2xhc3M9ImVyIHJlbGF0aW9uc2hpcExhYmVsIj5saW5rczwvdGV4dD48L3N2Zz4=", + "version": 2 + } + } +} \ No newline at end of file diff --git a/docs/database/schema.jpg b/docs/database/schema.jpg deleted file mode 100644 index 0c37c647..00000000 Binary files a/docs/database/schema.jpg and /dev/null differ diff --git a/docs/database/schema.mmd b/docs/database/schema.mmd new file mode 100644 index 00000000..c45aa17a --- /dev/null +++ b/docs/database/schema.mmd @@ -0,0 +1,53 @@ +erDiagram + anonymization_document { + UUID id PK + DATETIME created_at + DATETIME updated_at + TEXT name + } + + anonymization_paragraph { + UUID id PK + TEXT text + JSON prediction + JSON validation + DATETIME created_at + DATETIME updated_at + } + + anonymization_document_paragraph { + UUID id PK + UUID document_id PK, FK + UUID paragraph_id PK, FK + INTEGER order + } + + datapublic_document { + UUID id PK + JSON prediction + JSON validation + DATETIME created_at + DATETIME updated_at + } + + datapublic_paragraph { + UUID id PK + TEXT text + JSON prediction + JSON validation + DATETIME created_at + DATETIME updated_at + } + + datapublic_document_paragraph { + UUID id PK + UUID document_id PK, FK + UUID paragraph_id PK, FK + INTEGER order + } + + anonymization_document ||--o{ anonymization_document_paragraph : links + anonymization_paragraph ||--o{ anonymization_document_paragraph : links + + datapublic_document ||--o{ datapublic_document_paragraph : links + datapublic_paragraph ||--o{ datapublic_document_paragraph : links diff --git a/docs/database/schema.png b/docs/database/schema.png new file mode 100644 index 00000000..522c5e57 Binary files /dev/null and b/docs/database/schema.png differ diff --git a/docs/entities/README.md b/docs/entities/README.md new file mode 100644 index 00000000..e12055f7 --- /dev/null +++ b/docs/entities/README.md @@ -0,0 +1,17 @@ +# Entities +Language: **English** | [Español](../es/entities/README.md) + +This section documents the entity catalogs used by AymurAI workflows. + +## Entity catalogs by flow +- Datapublic: [datapublic/README.md](datapublic/README.md) +- Anonymizer: [anonymizer/README.md](anonymizer/README.md) + +## Notes +- `datapublic` entities describe the public-data extraction taxonomy used by the production `datapublic` pipeline. +- `anonymizer` entities describe the labels currently used by the anonymization flow for disambiguation and replacement. + +## Related docs +- Documentation index: [../README.md](../README.md) +- Pipelines index: [../pipelines/README.md](../pipelines/README.md) +- Models index: [../models/README.md](../models/README.md) diff --git a/docs/entities/anonymizer/README.md b/docs/entities/anonymizer/README.md new file mode 100644 index 00000000..2ae23e12 --- /dev/null +++ b/docs/entities/anonymizer/README.md @@ -0,0 +1,45 @@ +# Anonymizer Entities +Language: **English** | [Español](../../es/entities/anonymizer/README.md) + +Catalog of labels currently used by the anonymization flow. + +## Scope +These labels represent the entity types recognized and transformed by the production `flair-anonymizer` pipeline. They are the labels on which disambiguation, anonymization policies, and render policies operate. + +## Labels +| Label | Description | +|---|---| +| `PER` | Person name or person mention. | +| `EDAD` | Age. | +| `DNI` | Argentine national identity document number. | +| `NACIONALIDAD` | Nationality. | +| `ESTUDIOS` | Education or level of studies. | +| `DIRECCION` | Postal or street address. | +| `LOC` | Geographic location or place reference. | +| `TELEFONO` | Phone number. | +| `CORREO_ELECTRONICO` | Email address. | +| `FECHA` | Calendar date. | +| `NUM_EXPEDIENTE` | Case or file number. | +| `CUIJ` | Judicial unique case identifier. | +| `NUM_ACTUACION` | Proceeding or action number. | +| `NUM_MATRICULA` | Registration or professional license number. | +| `NOMBRE_ARCHIVO` | File name. | +| `TEXTO_ANONIMIZAR` | Free-form text span explicitly marked for anonymization when it does not fit a more specific structured label. | +| `USUARIX` | Username, account handle, or user identifier. | +| `LINK` | URL or web link. | +| `IP` | IP address. | +| `CUIT_CUIL` | Argentine tax or labor identifier. | +| `BANCO` | Bank name. | +| `CBU` | Argentine bank account CBU. | +| `NUM_CAJA_AHORRO` | Savings account number. | +| `MARCA_AUTOMOVIL` | Vehicle make or model reference. | +| `PATENTE_DOMINIO` | Vehicle license plate. | + +## Notes +- This catalog reflects the current anonymization token set used by the backend replacement utilities. +- Individual labels may be configured with per-label anonymization or disambiguation policies through `LabelPolicy`. + +## Related docs +- Entities index: [../README.md](../README.md) +- Anonymizer pipeline: [../../pipelines/anonymizer/README.md](../../pipelines/anonymizer/README.md) +- API reference: [../../api/README.md](../../api/README.md) diff --git a/docs/entities/datapublic/README.md b/docs/entities/datapublic/README.md new file mode 100644 index 00000000..578e1c30 --- /dev/null +++ b/docs/entities/datapublic/README.md @@ -0,0 +1,47 @@ +# Datapublic Entities +Language: **English** | [Español](../../es/entities/datapublic/README.md) + +Catalog of entities used by the datapublic extraction flow. + +## Scope +These entities correspond to the taxonomy extracted from judicial rulings by the production `datapublic` pipeline. They are used across the Flair NER model, decision post-processing, validation UI, and related dataset tooling. + +## Entities +| Entity | Description | +|---|---| +| `ART_INFRINGIDO` | Article(s) or legal provision(s) infringed in the case. | +| `CONDUCTA` | Action associated with the offence, contravention, or misdemeanor described in the infringed article. | +| `CONDUCTA_DESCRIPCION` | Additional characterization of the conduct, such as aggravating circumstances or modality details. | +| `DECISION` | Text fragment denoting a judicial decision. In production this is a synthetic label emitted by the decision classifier. | +| `DETALLE` | Detail that specifies what was resolved within `OBJETO_DE_LA_RESOLUCION`. | +| `EDAD_AL_MOMENTO_DEL_HECHO` | Age of the person at the time of the event. | +| `FECHA_DE_NACIMIENTO` | Date of birth. | +| `FECHA_DEL_HECHO` | Date on which the reported event occurred. | +| `FECHA_RESOLUCION` | Date of the resolution; for oral hearings, the hearing start date. | +| `FRASES_AGRESION` | Quoted or paraphrased phrases described as verbal aggression within the facts of the case. | +| `GENERO` | Gender. | +| `HIJOS_HIJAS_EN_COMUN` | Whether the accused person and the complainant have children in common. | +| `HORA_DE_CIERRE` | End time of the hearing. | +| `HORA_DE_INICIO` | Start time of the hearing. | +| `LUGAR_DEL_HECHO` | Physical or mediated location where the facts occurred. | +| `MODALIDAD_DE_LA_VIOLENCIA` | Modality in which violence manifests, such as domestic, institutional, labor, media, or public-space violence. | +| `N_EXPTE_EJE` | Case or file identifier. | +| `NACIONALIDAD` | Nationality. | +| `NIVEL_INSTRUCCION` | Level of formal education attained by the person. | +| `NOMBRE` | Person name. | +| `OBJETO_DE_LA_RESOLUCION` | What the court resolved about. | +| `PERSONA_ACUSADA_NO_DETERMINADA` | Non-natural or undetermined accused party, such as a legal entity or online account. | +| `RELACION_Y_TIPO_ENTRE_ACUSADO/A_Y_DENUNCIANTE` | Relationship type between the accused person and the complainant. | +| `TIPO_DE_RESOLUCION` | Type of resolution, such as interlocutory or final resolution. | +| `VIOLENCIA_DE_GENERO` | Whether the investigated facts occur within a gender-violence context. | + +## Subcategories and validation fields +- Many of these entities have subclassification or validation-specific controlled vocabularies. +- Historical validation options are captured in the Label Studio template: [../../../resources/annotations/label-studio/datapublic/label-studio-config.xml](../../../resources/annotations/label-studio/datapublic/label-studio-config.xml) +- A broader explanatory glossary is available here (Spanish): https://docs.google.com/document/d/123B9T2abCEqBaxxOl5c7HBJZRdIMtKDWo6IKHIVil04/edit + +## Related docs +- Entities index: [../README.md](../README.md) +- Datapublic pipeline: [../../pipelines/datapublic/README.md](../../pipelines/datapublic/README.md) +- Decision model card: [../../models/decision-model-card.md](../../models/decision-model-card.md) +- Flair model card: [../../models/flair-model-card.md](../../models/flair-model-card.md) diff --git a/docs/es/README.md b/docs/es/README.md new file mode 100644 index 00000000..7d3c2710 --- /dev/null +++ b/docs/es/README.md @@ -0,0 +1,29 @@ +# Índice de Documentación +Idioma: [English](../README.md) | **Español** + +Este índice cubre la documentación operativa para `v1.5.0`. + +## Documentos principales +- Entrada del repositorio: [../../README.es.md](../../README.es.md) +- Referencia de API: [api/README.md](api/README.md) +- Índice de pipelines: [pipelines/README.md](pipelines/README.md) +- Flujo anonymizer: [pipelines/anonymizer/README.md](pipelines/anonymizer/README.md) +- Flujo datapublic: [pipelines/datapublic/README.md](pipelines/datapublic/README.md) +- Esquema de base de datos interna: [database/README.md](database/README.md) +- Desarrollo y empaquetado del frontend: [../../frontend/README.md](../../frontend/README.md) + +## Entidades +- Índice de entidades: [entities/README.md](entities/README.md) +- Entidades de datapublic: [entities/datapublic/README.md](entities/datapublic/README.md) +- Entidades de anonymizer: [entities/anonymizer/README.md](entities/anonymizer/README.md) + +## Modelos +- Índice de modelos: [models/README.md](models/README.md) +- Model card del NER de anonymizer: [models/anonymizer-model-card.md](models/anonymizer-model-card.md) +- Model card de Flair NER: [models/flair-model-card.md](models/flair-model-card.md) +- Model card de Decision: [models/decision-model-card.md](models/decision-model-card.md) + +## Gobernanza +- Contribución: [../CONTRIBUTING.md](../CONTRIBUTING.md) +- Seguridad y ética: [../SECURITY.md](../SECURITY.md) +- Código de conducta: [../CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md) diff --git a/docs/es/api/README.md b/docs/es/api/README.md new file mode 100644 index 00000000..afb84e97 --- /dev/null +++ b/docs/es/api/README.md @@ -0,0 +1,321 @@ +# Referencia de API +Idioma: [English](../../api/README.md) | **Español** + +Este documento describe la API pública actualmente montada en `aymurai/api/main.py` + `aymurai/api/core.py`. + +## Base URL y OpenAPI +- Base local: `http://localhost:8899` +- Swagger UI: `http://localhost:8899/api/docs` +- OpenAPI JSON: `http://localhost:8899/api/openapi.json` + +## Endpoints públicos (montados) + +| Método | Path | Propósito | +|---|---|---| +| `GET` | `/api/server/healthcheck` | Liveness del servicio | +| `GET` | `/api/server/stats/summary` | Métricas de CPU/memoria | +| `POST` | `/api/document-extract` | Alias deprecado de `/api/misc/document-extract` | +| `POST` | `/api/misc/document-extract` | Extrae párrafos normalizados de un documento | +| `POST` | `/api/anonymizer/predict` | Predicción NER por párrafo | +| `POST` | `/api/anonymizer/disambiguate` | Desambiguación canónica + merge de políticas | +| `POST` | `/api/anonymizer/validation` | Obtiene validación manual por párrafo | +| `POST` | `/api/anonymizer/anonymize-document` | Compila y exporta documento anonimizado | +| `POST` | `/api/datapublic/predict/{document_id}` | Predicción para flujo data-public | +| `GET` | `/api/datapublic/validation/document/{document_id}` | Lee validación a nivel documento | +| `POST` | `/api/datapublic/validation/document/{document_id}` | Guarda validación a nivel documento | +| `POST` | `/api/convert/pdf/odt` | Convierte PDF a ODT | +| `POST` | `/api/convert/pdf/docx` | Convierte PDF a DOCX | +| `POST` | `/api/convert/docx/odt` | Convierte DOCX a ODT | +| `POST` | `/api/convert/docx/pdf` | Convierte DOCX a PDF | +| `POST` | `/api/convert/odt/pdf` | Convierte ODT a PDF | +| `POST` | `/api/convert/odt/docx` | Convierte ODT a DOCX | + +## Contratos de datos principales + +Nota: los snippets JSON de abajo son ejemplos mínimos válidos. Los payloads reales pueden incluir campos adicionales según el endpoint y la etapa de procesamiento. + +### `TextRequest` +```json +{ + "text": "Acusado: Ramiro Marrón DNI 34.555.666." +} +``` + +### `EntityAttributes` (campos relevantes) +```json +{ + "aymurai_label": "PER", + "aymurai_label_subclass": [], + "aymurai_method": "flair", + "aymurai_score": 0.97, + "canonical_entity_id": "a3f8b60f-8e7e-4c8b-9de2-ec8dd3f44c12", + "aymurai_label_instance": 1, + "aymurai_disambiguation": "fuzzy", + "aymurai_anonymize": true +} +``` + +### `DocLabel` +```json +{ + "text": "Ramiro Marrón", + "start_char": 9, + "end_char": 22, + "attrs": { + "aymurai_label": "PER" + } +} +``` + +### `DocumentInformation` +```json +{ + "document": "Acusado: Ramiro Marrón DNI 34.555.666.", + "labels": [ + { + "text": "Ramiro Marrón", + "start_char": 9, + "end_char": 22, + "attrs": { + "aymurai_label": "PER" + } + } + ] +} +``` + +### `LabelPolicy` +```json +{ + "anonymize": true, + "disambiguation": "fuzzy", + "use_subclass_when_available": true +} +``` + +### `RenderPolicy` +```json +{ + "suffix_mode": "auto", + "suffix_threshold": 1 +} +``` + +### `DocumentAnnotations` +```json +{ + "data": [ + { + "document": "...", + "labels": [] + } + ], + "label_policies": { + "PER": { + "anonymize": true, + "disambiguation": "fuzzy", + "use_subclass_when_available": false + } + }, + "render_policy": { + "suffix_mode": "auto", + "suffix_threshold": 1 + } +} +``` + +## Detalle de endpoints y ejemplos + +### Server + +#### `GET /api/server/healthcheck` +- Respuesta `200`: + +```json +{"status": "ok"} +``` + +```bash +curl -s http://localhost:8899/api/server/healthcheck +``` + +#### `GET /api/server/stats/summary` +- Respuesta `200` (forma): + +```json +{ + "is_docker": true, + "cpu_core_limit": 4, + "cpu_usage_percent": 0.0, + "memory_limit_mb": 4096.0, + "memory_usage_mb": 823.5 +} +``` + +```bash +curl -s http://localhost:8899/api/server/stats/summary +``` + +### Extracción de documentos + +#### `POST /api/misc/document-extract` +#### `POST /api/document-extract` (alias deprecado) +- Request: `multipart/form-data` con `file` +- MIME types soportados en extracción: DOCX, ODT, PDF +- Respuesta `200`: + +```json +{ + "document": ["Párrafo 1", "Párrafo 2"], + "document_id": "f2b25507-cf88-5b11-8f2a-c0b6f940b7f8" +} +``` + +```bash +curl -s -X POST \ + -F "file=@/resources/data/sample/document-01.docx" \ + http://localhost:8899/api/misc/document-extract +``` + +Errores comunes: +- `504` timeout de extracción +- `500` errores del extractor/internos + +### Anonymizer + +#### `POST /api/anonymizer/predict` +- Body: `TextRequest` +- Query param: `use_cache=true|false` (default `true`) +- Respuesta `200`: `DocumentInformation` + +```bash +curl -s -X POST "http://localhost:8899/api/anonymizer/predict?use_cache=true" \ + -H "Content-Type: application/json" \ + -d '{"text":"Acusado: Ramiro Marrón DNI 34.555.666."}' +``` + +#### `POST /api/anonymizer/disambiguate` +- Body request: + +```json +{ + "paragraphs": [ + { + "document": "Acusado: Ramiro Marrón DNI 34.555.666.", + "labels": [] + } + ], + "label_policies": { + "PER": { + "anonymize": true, + "disambiguation": "fuzzy", + "use_subclass_when_available": false + } + } +} +``` + +- Respuesta `200`: `DocumentAnnotations` (incluye `data` y `label_policies` efectivas) + +```bash +curl -s -X POST http://localhost:8899/api/anonymizer/disambiguate \ + -H "Content-Type: application/json" \ + -d '{"paragraphs":[{"document":"Acusado: Ramiro Marrón DNI 34.555.666.","labels":[]}],"label_policies":{"PER":{"anonymize":true,"disambiguation":"fuzzy"}}}' +``` + +#### `POST /api/anonymizer/validation` +- Body: `TextRequest` +- Respuesta `200`: `list[DocLabel] | null` + +```bash +curl -s -X POST http://localhost:8899/api/anonymizer/validation \ + -H "Content-Type: application/json" \ + -d '{"text":"Acusado: Ramiro Marrón DNI 34.555.666."}' +``` + +#### `POST /api/anonymizer/anonymize-document` +- Request: `multipart/form-data` + - `file`: documento original (`.docx` o `.pdf`) + - `annotations`: string JSON serializado de `DocumentAnnotations` +- Respuesta `200`: `.odt` anonimizado para entrada DOCX o `.pdf` anonimizado para entrada PDF + +```bash +curl -X POST http://localhost:8899/api/anonymizer/anonymize-document \ + -F "file=@/resources/data/sample/document-01.docx" \ + -F 'annotations={"data":[{"document":"Acusado: Ramiro Marrón DNI 34.555.666.","labels":[]}],"label_policies":{"PER":{"anonymize":true,"disambiguation":"fuzzy"}},"render_policy":{"suffix_mode":"auto","suffix_threshold":1}}' +``` + +Errores comunes: +- `400` payload multipart inválido, formato no soportado o documento inválido +- `500` errores de anonimización/conversión + +### Data-Public + +#### `POST /api/datapublic/predict/{document_id}` +- Path param: `document_id` (`UUID5`) +- Body: `TextRequest` +- Query param: `use_cache=true|false` (default `true`) +- Respuesta `200`: `DocumentInformation` + +```bash +curl -s -X POST "http://localhost:8899/api/datapublic/predict/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc?use_cache=true" \ + -H "Content-Type: application/json" \ + -d '{"text":"Buenos Aires, 17 de noviembre de 2024"}' +``` + +#### `GET /api/datapublic/validation/document/{document_id}` +- Respuesta `200`: objeto o `null` +- Respuesta `404`: documento inexistente + +```bash +curl -s http://localhost:8899/api/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc +``` + +#### `POST /api/datapublic/validation/document/{document_id}` +- Body: objeto JSON libre (se persiste como validación a nivel documento) +- Respuesta `200`: body vacío + +```bash +curl -s -X POST http://localhost:8899/api/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc \ + -H "Content-Type: application/json" \ + -d '{"materia":"penal","violencia_de_genero":"si"}' +``` + +### Conversión de documentos + +Todos los endpoints de conversión usan `multipart/form-data` con campo `file`. + +| Método | Path | Input | Output | +|---|---|---|---| +| `POST` | `/api/convert/pdf/odt` | `.pdf` | `.odt` | +| `POST` | `/api/convert/pdf/docx` | `.pdf` | `.docx` | +| `POST` | `/api/convert/docx/odt` | `.docx` | `.odt` | +| `POST` | `/api/convert/docx/pdf` | `.docx` | `.pdf` | +| `POST` | `/api/convert/odt/pdf` | `.odt` | `.pdf` | +| `POST` | `/api/convert/odt/docx` | `.odt` | `.docx` | + +Para endpoints con input PDF, query param opcional: +- `backend=libreoffice|pandoc` (default: `libreoffice`) + +Ejemplo: + +```bash +curl -X POST "http://localhost:8899/api/convert/pdf/docx?backend=libreoffice" \ + -F "file=@input.pdf" -o output.docx +``` + +Errores comunes: +- `400` extensión de entrada no soportada +- `500` falla de herramienta de conversión + +## Legacy / no pública (no montada) +Las siguientes rutas existen en código pero no están incluidas en `core.router` en runtime: + +- `aymurai/api/endpoints/routers/datapublic/dataset.py` + - define `/api/datapublic/dataset/*`, pero ese router no está montado. +- `aymurai/api/endpoints/routers/anonymizer/database.py` + - define `/api/anonymizer/database/*`, pero su include está comentado. +- `aymurai/api/endpoints/routers/database/*` + - rutas administrativas de DB, sin montaje en `core.router`. + +Estas rutas deben tratarse como caminos internos/legacy hasta su exposición explícita. diff --git a/docs/es/database/README.md b/docs/es/database/README.md new file mode 100644 index 00000000..b263ac1d --- /dev/null +++ b/docs/es/database/README.md @@ -0,0 +1,113 @@ +# Base de Datos Interna +Idioma: [English](../../database/README.md) | **Español** + +Este documento describe la persistencia interna usada por la API en runtime. + +## Motor y migraciones +- ORM: `SQLModel` (backend `sqlalchemy`) +- Herramienta de migración: Alembic +- Setting de runtime: `SQLALCHEMY_DATABASE_URI` +- URI por defecto: `sqlite:////resources/cache/sqlite/database.db` +- Comportamiento al iniciar: + 1. La API valida conectividad de DB. + 2. Si falta el archivo SQLite, crea los directorios padre. + 3. Ejecuta `alembic upgrade head` al iniciar. + +Código relacionado: +- `aymurai/settings.py` +- `aymurai/api/startup/database.py` +- `aymurai/api/main.py` +- `aymurai/database/versions/13f78d08e925_create_database.py` + +## Diagrama ER +![Diagrama ER de la base de datos](../../database/schema.png) + +Fuente editable: [../../database/schema.mmd](../../database/schema.mmd) + +## Tablas + +### `anonymization_document` +| Columna | Tipo | Nulo | Notas | +|---|---|---|---| +| `id` | `UUID` | no | Primary key | +| `created_at` | `DATETIME` | no | Default server `CURRENT_TIMESTAMP` | +| `updated_at` | `DATETIME` | sí | Se actualiza ante cambios | +| `name` | `TEXT` | no | Nombre de archivo original | + +### `anonymization_paragraph` +| Columna | Tipo | Nulo | Notas | +|---|---|---|---| +| `id` | `UUID` | no | Primary key, derivada del hash del texto | +| `text` | `TEXT` | no | Texto normalizado del párrafo | +| `prediction` | `JSON` | sí | Predicciones del modelo (`list[DocLabel]`) | +| `validation` | `JSON` | sí | Etiquetas validadas manualmente (`list[DocLabel]`) | +| `created_at` | `DATETIME` | no | Default server `CURRENT_TIMESTAMP` | +| `updated_at` | `DATETIME` | sí | Se actualiza ante cambios | + +### `anonymization_document_paragraph` +| Columna | Tipo | Nulo | Notas | +|---|---|---|---| +| `id` | `UUID` | no | Identificador del vínculo | +| `document_id` | `UUID` | no | FK -> `anonymization_document.id` | +| `paragraph_id` | `UUID` | no | FK -> `anonymization_paragraph.id` | +| `order` | `INTEGER` | sí | Orden del párrafo en documento origen | + +La clave primaria es compuesta por `id`, `document_id`, `paragraph_id`. + +### `datapublic_document` +| Columna | Tipo | Nulo | Notas | +|---|---|---|---| +| `id` | `UUID` | no | Primary key (identificador del documento) | +| `prediction` | `JSON` | sí | Payload reservado de predicción a nivel documento; el router público actual no lo escribe | +| `validation` | `JSON` | sí | Payload de validación a nivel documento | +| `created_at` | `DATETIME` | no | Default server `CURRENT_TIMESTAMP` | +| `updated_at` | `DATETIME` | sí | Se actualiza ante cambios | + +### `datapublic_paragraph` +| Columna | Tipo | Nulo | Notas | +|---|---|---|---| +| `id` | `UUID` | no | Primary key, derivada del hash del texto | +| `text` | `TEXT` | no | Texto normalizado del párrafo | +| `prediction` | `JSON` | sí | Predicciones del modelo (`list[DocLabel]`) | +| `validation` | `JSON` | sí | Reservado para validación por párrafo; la ruta pública hoy existe solo como lógica legacy comentada | +| `created_at` | `DATETIME` | no | Default server `CURRENT_TIMESTAMP` | +| `updated_at` | `DATETIME` | sí | Se actualiza ante cambios | + +### `datapublic_document_paragraph` +| Columna | Tipo | Nulo | Notas | +|---|---|---|---| +| `id` | `UUID` | no | Identificador del vínculo | +| `document_id` | `UUID` | no | FK -> `datapublic_document.id` | +| `paragraph_id` | `UUID` | no | FK -> `datapublic_paragraph.id` | +| `order` | `INTEGER` | sí | Orden del párrafo en documento origen | + +La clave primaria es compuesta por `id`, `document_id`, `paragraph_id`. + +## Mapeo endpoint -> persistencia + +### Anonymizer +- `POST /api/anonymizer/predict` + - Lee `anonymization_paragraph` por UUID de párrafo. + - Escribe `anonymization_paragraph.prediction` cuando el cache está activo. +- `POST /api/anonymizer/disambiguate` + - Escribe predicciones desambiguadas en `anonymization_paragraph.prediction`. +- `POST /api/anonymizer/validation` + - Lee `anonymization_paragraph.validation`. +- `POST /api/anonymizer/anonymize-document` + - Escribe `anonymization_paragraph.validation`. + - Crea `anonymization_document` con clave derivada del hash del contenido binario subido. + - Crea vínculos en `anonymization_document_paragraph`. + +### Data-public +- `POST /api/datapublic/predict/{document_id}` + - Usa el `document_id` provisto por el cliente como primary key del documento. + - Asegura existencia de `datapublic_document` cuando `use_cache=true`. + - Escribe `datapublic_paragraph.prediction` cuando `use_cache=true`. + - Escribe vínculo en `datapublic_document_paragraph` cuando `use_cache=true`. +- `GET /api/datapublic/validation/document/{document_id}` + - Lee `datapublic_document.validation`. +- `POST /api/datapublic/validation/document/{document_id}` + - Hace upsert de `datapublic_document.validation`. + +## Nota legacy +Los módulos de rutas para CRUD de dataset (`/api/datapublic/dataset/*`) existen en código pero no están montados en el router público. No deben tratarse como API pública activa hasta su exposición en `core.router`. diff --git a/docs/es/entities/README.md b/docs/es/entities/README.md new file mode 100644 index 00000000..59fb1247 --- /dev/null +++ b/docs/es/entities/README.md @@ -0,0 +1,17 @@ +# Entidades +Idioma: [English](../../entities/README.md) | **Español** + +Esta sección documenta los catálogos de entidades utilizados por los distintos flujos de AymurAI. + +## Catálogos de entidades por flujo +- Datapublic: [datapublic/README.md](datapublic/README.md) +- Anonymizer: [anonymizer/README.md](anonymizer/README.md) + +## Notas +- Las entidades de `datapublic` describen la taxonomía de extracción de datos públicos usada por el pipeline de producción `datapublic`. +- Las entidades de `anonymizer` describen las labels usadas actualmente por el flujo de anonimización para desambiguación y reemplazo. + +## Documentación relacionada +- Índice de documentación: [../README.md](../README.md) +- Índice de pipelines: [../pipelines/README.md](../pipelines/README.md) +- Índice de modelos: [../models/README.md](../models/README.md) diff --git a/docs/es/entities/anonymizer/README.md b/docs/es/entities/anonymizer/README.md new file mode 100644 index 00000000..b4fee224 --- /dev/null +++ b/docs/es/entities/anonymizer/README.md @@ -0,0 +1,45 @@ +# Entidades de Anonymizer +Idioma: [English](../../../entities/anonymizer/README.md) | **Español** + +Catálogo de labels usadas actualmente por el flujo de anonimización. + +## Alcance +Estas labels representan los tipos de entidad reconocidos y transformados por el pipeline de producción `flair-anonymizer`. Son las labels sobre las que operan la desambiguación, las políticas de anonimización y las políticas de renderizado. + +## Labels +| Label | Descripción | +|---|---| +| `PER` | Nombre de persona o mención de persona. | +| `EDAD` | Edad. | +| `DNI` | Número de documento nacional de identidad argentino. | +| `NACIONALIDAD` | Nacionalidad. | +| `ESTUDIOS` | Estudios o nivel educativo. | +| `DIRECCION` | Dirección postal o calle. | +| `LOC` | Ubicación geográfica o referencia de lugar. | +| `TELEFONO` | Número de teléfono. | +| `CORREO_ELECTRONICO` | Dirección de correo electrónico. | +| `FECHA` | Fecha calendario. | +| `NUM_EXPEDIENTE` | Número de expediente o causa. | +| `CUIJ` | Identificador judicial único de causa. | +| `NUM_ACTUACION` | Número de actuación. | +| `NUM_MATRICULA` | Número de matrícula o licencia profesional. | +| `NOMBRE_ARCHIVO` | Nombre de archivo. | +| `TEXTO_ANONIMIZAR` | Fragmento de texto libre marcado explícitamente para anonimización cuando no encaja en una label estructurada más específica. | +| `USUARIX` | Nombre de usuario, handle o identificador de cuenta. | +| `LINK` | URL o enlace web. | +| `IP` | Dirección IP. | +| `CUIT_CUIL` | Identificador tributario o laboral argentino. | +| `BANCO` | Nombre de entidad bancaria. | +| `CBU` | Clave Bancaria Uniforme argentina. | +| `NUM_CAJA_AHORRO` | Número de caja de ahorro. | +| `MARCA_AUTOMOVIL` | Referencia a marca o modelo de vehículo. | +| `PATENTE_DOMINIO` | Patente o dominio vehicular. | + +## Notas +- Este catálogo refleja el conjunto actual de tokens de anonimización usado por las utilidades de reemplazo del backend. +- Cada label puede configurarse con políticas específicas de anonimización o desambiguación mediante `LabelPolicy`. + +## Documentación relacionada +- Índice de entidades: [../README.md](../README.md) +- Pipeline anonymizer: [../../pipelines/anonymizer/README.md](../../pipelines/anonymizer/README.md) +- Referencia API: [../../api/README.md](../../api/README.md) diff --git a/docs/es/entities/datapublic/README.md b/docs/es/entities/datapublic/README.md new file mode 100644 index 00000000..e645344f --- /dev/null +++ b/docs/es/entities/datapublic/README.md @@ -0,0 +1,47 @@ +# Entidades de Datapublic +Idioma: [English](../../../entities/datapublic/README.md) | **Español** + +Catálogo de entidades utilizadas por el flujo de extracción de datapublic. + +## Alcance +Estas entidades corresponden a la taxonomía extraída de resoluciones judiciales por el pipeline de producción `datapublic`. Se usan a lo largo del modelo Flair NER, el postprocesamiento de decisiones, la UI de validación y el tooling relacionado con el dataset. + +## Entidades +| Entidad | Descripción | +|---|---| +| `ART_INFRINGIDO` | Artículo(s) o norma(s) legal(es) infringida(s) en el caso. | +| `CONDUCTA` | Acción asociada al delito, contravención o falta descripta en el artículo infringido. | +| `CONDUCTA_DESCRIPCION` | Caracterización adicional de la conducta, como agravantes o detalles de modalidad. | +| `DECISION` | Fragmento de texto que denota una decisión judicial. En producción es una label sintética emitida por el clasificador de decisiones. | +| `DETALLE` | Detalle que especifica qué se resolvió dentro de `OBJETO_DE_LA_RESOLUCION`. | +| `EDAD_AL_MOMENTO_DEL_HECHO` | Edad de la persona al momento del hecho. | +| `FECHA_DE_NACIMIENTO` | Fecha de nacimiento. | +| `FECHA_DEL_HECHO` | Fecha en que ocurrió el hecho denunciado. | +| `FECHA_RESOLUCION` | Fecha de la resolución; en audiencias orales, la fecha de inicio de la audiencia. | +| `FRASES_AGRESION` | Frases citadas o parafraseadas como agresión verbal dentro de los hechos del caso. | +| `GENERO` | Género. | +| `HIJOS_HIJAS_EN_COMUN` | Si la persona acusada y la denunciante tienen hijos/as en común. | +| `HORA_DE_CIERRE` | Hora de finalización de la audiencia. | +| `HORA_DE_INICIO` | Hora de inicio de la audiencia. | +| `LUGAR_DEL_HECHO` | Lugar físico o mediado donde ocurrieron los hechos. | +| `MODALIDAD_DE_LA_VIOLENCIA` | Modalidad en la que se manifiesta la violencia, como violencia doméstica, institucional, laboral, mediática o en espacio público. | +| `N_EXPTE_EJE` | Identificador del expediente o caso. | +| `NACIONALIDAD` | Nacionalidad. | +| `NIVEL_INSTRUCCION` | Nivel de estudios formales alcanzado por la persona. | +| `NOMBRE` | Nombre de persona. | +| `OBJETO_DE_LA_RESOLUCION` | Sobre qué resolvió el juzgado. | +| `PERSONA_ACUSADA_NO_DETERMINADA` | Parte acusada no determinada o no humana, como una persona jurídica o una cuenta en línea. | +| `RELACION_Y_TIPO_ENTRE_ACUSADO/A_Y_DENUNCIANTE` | Tipo de vínculo entre la persona acusada y la denunciante. | +| `TIPO_DE_RESOLUCION` | Tipo de resolución, como interlocutoria o definitiva. | +| `VIOLENCIA_DE_GENERO` | Si los hechos investigados ocurren en un contexto de violencia de género. | + +## Subcategorías y campos de validación +- Muchas de estas entidades tienen subclasificaciones o vocabularios controlados específicos para validación. +- Las opciones históricas de validación están capturadas en el template de Label Studio: [../../../../resources/annotations/label-studio/datapublic/label-studio-config.xml](../../../../resources/annotations/label-studio/datapublic/label-studio-config.xml) +- Un glosario explicativo más amplio está disponible aquí: https://docs.google.com/document/d/123B9T2abCEqBaxxOl5c7HBJZRdIMtKDWo6IKHIVil04/edit + +## Documentación relacionada +- Índice de entidades: [../README.md](../README.md) +- Pipeline datapublic: [../../pipelines/datapublic/README.md](../../pipelines/datapublic/README.md) +- Model card de Decision: [../../models/decision-model-card.md](../../models/decision-model-card.md) +- Model card de Flair: [../../models/flair-model-card.md](../../models/flair-model-card.md) diff --git a/docs/es/models/README.md b/docs/es/models/README.md new file mode 100644 index 00000000..a4bcd14f --- /dev/null +++ b/docs/es/models/README.md @@ -0,0 +1,19 @@ +# Modelos +Idioma: [English](../../models/README.md) | **Español** + +Esta sección documenta los modelos individuales utilizados por el backend. + +## Documentación disponible +- Model card del NER de anonymizer: [anonymizer-model-card.md](anonymizer-model-card.md) +- Flair NER: [flair-model-card.md](flair-model-card.md) +- Clasificador de decisiones: [decision-model-card.md](decision-model-card.md) + +## Uso actual en producción +- `flair-anonymizer` usa la model card del NER de anonymizer documentada aquí. +- `datapublic` usa el modelo Flair NER, el clasificador de decisiones y encoders sentence-transformer multilingües para subcategorización basada en embeddings. + +## Documentación relacionada +- Índice de documentación: [../README.md](../README.md) +- Índice de pipelines: [../pipelines/README.md](../pipelines/README.md) +- Flujo anonymizer: [../pipelines/anonymizer/README.md](../pipelines/anonymizer/README.md) +- Flujo datapublic: [../pipelines/datapublic/README.md](../pipelines/datapublic/README.md) diff --git a/docs/es/models/anonymizer-model-card.md b/docs/es/models/anonymizer-model-card.md new file mode 100644 index 00000000..0f934578 --- /dev/null +++ b/docs/es/models/anonymizer-model-card.md @@ -0,0 +1,172 @@ +--- +license: mit +language: +- es +tags: +- flair +- token-classification +- sequence-tagger-model +- anonymization +- judicial-text +datasets: +- ArJuzPCyF10 +metrics: +- precision +- recall +- f1-score +widget: +- text: 1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas. +library_name: flair +pipeline_tag: token-classification +--- + +Idioma: [English](../../models/anonymizer-model-card.md) | **Español** + +# Descripción del modelo + +Este modelo es el componente NER usado por el pipeline de producción `flair-anonymizer`. +Detecta spans que deben anonimizarse en documentos judiciales antes de la desambiguación, la validación manual y el render final del documento. + +Siguiendo las guías de Flair para entrenamiento de NER, el modelo se entrenó sobre [embeddings BETO](https://huggingface.co/dccuchile/bert-base-spanish-wwm-uncased), una versión en español de BERT, con una arquitectura BiLSTM-CRF. + +Este modelo fue desarrollado por [{ collective.ai }](https://collectiveai.io) como parte del proyecto [AymurAI](https://aymurai.info) de [DataGenero](https://datagenero.org). + +## Usos previstos y limitaciones + +AymurAI busca ayudar a abordar la falta de datos disponibles sobre resoluciones de violencia de género (VG) en América Latina. En el flujo de anonimización, el objetivo inmediato de este modelo es identificar spans sensibles en documentos legales para que puedan ser revisados y reemplazados antes de su uso posterior. + +AymurAI sigue siendo un sistema específico de dominio. Sus capacidades se limitan a la anonimización, recolección y análisis semiautomatizados de datos judiciales, y la salida puede verse afectada por la calidad de la anotación, la heterogeneidad documental, errores de OCR o extracción y la disponibilidad de datos de entrenamiento representativos. + +Este modelo fue entrenado con un dataset cerrado de un juzgado penal argentino. Esa especificidad de dominio mejora el desempeño en el contexto objetivo, pero también implica que el modelo puede no transferir bien a otras jurisdicciones, estilos documentales o culturas jurídicas. + +## Comportamiento en producción + +En producción, este modelo se carga a través de `aymurai.models.flair.core.FlairModel` desde: + +- `resources/pipelines/production/flair-anonymizer/pipeline.json` +- model path: `aymurai/anonymizer-beto-cased-flair` + +Sus predicciones de spans se postprocesan luego con: + +- `aymurai.transforms.anonymization_postprocess.core.AnonymizationEntityCleaner` +- `aymurai.transforms.datetime_formatter.core.DatetimeFormatter` + +Esas predicciones alimentan el resto del flujo de anonimización: + +1. `POST /api/anonymizer/predict` ejecuta la extracción de spans. +2. `POST /api/anonymizer/disambiguate` asigna IDs canónicos y metadatos efectivos por label. +3. La revisión manual puede editar labels, `label_policies` y `render_policy`. +4. `POST /api/anonymizer/anonymize-document` aplica los reemplazos sobre el documento de salida. + +# Uso + +## Cómo usar el modelo en Flair + +Requiere **[Flair](https://github.com/flairNLP/flair/)**. +Instalación: `pip install flair`. + +```python +from flair.data import Sentence +from flair.models import SequenceTagger + +tagger = SequenceTagger.load("aymurai/anonymizer-beto-cased-flair") + +sentence = Sentence( + "1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento " + "de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO " + "MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves " + "agravadas, amenazas simples y agravadas por el uso de armas." +) + +tagger.predict(sentence) + +for entity in sentence.get_spans("ner"): + print(entity) +``` + +Esto produce una salida similar a: + +```text +Span[22:25]: "EZEQUIEL CAMILO MARCONNI" -> PER (0.9541) +Span[27:28]: "11.222.333" -> DNI (1.0) +``` + +## Uso del modelo en un pipeline de AymurAI + +```python +from aymurai.pipeline import AymurAIPipeline + +pipeline = AymurAIPipeline.load("/resources/pipelines/production/flair-anonymizer") + +item = { + "path": "dummy", + "data": { + "doc.text": ( + "Acusado: Ramiro Marrón DNI 34.555.666. " + "Fecha: 17 de noviembre de 2024." + ) + }, +} + +processed = pipeline.preprocess([item]) +processed = pipeline.predict_single(processed[0]) +processed = pipeline.postprocess([processed]) + +print(processed[0]["predictions"]["entities"]) +``` + +# Entidades y métricas + +## Descripción + +Consultá el catálogo de entidades de anonymizer ([en](../../entities/anonymizer/README.md)|[es](../entities/anonymizer/README.md)). + +## Datos + +El modelo fue entrenado con un dataset de 535 resoluciones judiciales de un juzgado penal argentino. + +Dada la naturaleza de los datos (datos personales, características de la denuncia y protección de víctimas), los documentos se mantienen privados. + +## Métricas + +Las siguientes métricas por label provienen de la model card publicada en Hugging Face para esta familia de modelos de anonymizer y deben interpretarse como métricas NER a nivel modelo, no como calidad end-to-end de la anonimización. + +| label | precision | recall | f1-score | +|---|---:|---:|---:| +| `BANCO` | 1.00 | 0.90 | 0.95 | +| `CBU` | 0.92 | 0.92 | 0.92 | +| `CORREO_ELECTRONICO` | 1.00 | 1.00 | 1.00 | +| `CUIJ` | 1.00 | 1.00 | 1.00 | +| `CUIT_CUIL` | 1.00 | 1.00 | 1.00 | +| `DIRECCION` | 0.97 | 0.85 | 0.91 | +| `DNI` | 0.96 | 1.00 | 0.98 | +| `EDAD` | 1.00 | 0.95 | 0.97 | +| `ESTUDIOS` | 1.00 | 1.00 | 1.00 | +| `FECHA` | 1.00 | 0.99 | 1.00 | +| `LINK` | 1.00 | 0.94 | 0.97 | +| `LOC` | 0.99 | 0.72 | 0.83 | +| `MARCA_AUTOMOVIL` | 0.95 | 1.00 | 0.97 | +| `NACIONALIDAD` | 1.00 | 0.94 | 0.97 | +| `NUM_ACTUACION` | 0.84 | 0.96 | 0.90 | +| `NUM_CAJA_AHORRO` | 0.00 | 0.00 | 0.00 | +| `NUM_EXPEDIENTE` | 0.98 | 0.92 | 0.95 | +| `NUM_MATRICULA` | 0.33 | 0.50 | 0.40 | +| `PATENTE_DOMINIO` | 1.00 | 1.00 | 1.00 | +| `PER` | 0.98 | 0.97 | 0.98 | +| `TELEFONO` | 0.97 | 1.00 | 0.99 | +| `TEXTO_ANONIMIZAR` | 0.98 | 0.61 | 0.75 | +| `macro avg` | 0.91 | 0.88 | 0.89 | + +# Cita + +Por favor citá [el siguiente paper](https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view) al utilizar AymurAI: + +```bibtex +@techreport{feldfeber2022, + author = {Feldfeber, Ivana and Quiroga, Yasm{\'i}n Bel{\'e}n and Guevara, Clarissa and Ciolfi Felice, Marianela}, + title = {Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico}, + institution = {DataGenero}, + year = {2022}, + url = {https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view} +} +``` diff --git a/docs/es/models/decision-model-card.md b/docs/es/models/decision-model-card.md new file mode 100644 index 00000000..0735c450 --- /dev/null +++ b/docs/es/models/decision-model-card.md @@ -0,0 +1,217 @@ +--- +license: mit +language: +- es +tags: +- text-classification +- embeddingbag +- binary-classification +- judicial-text +datasets: +- ArJuzPCyF10 +metrics: +- f1 +widget: +- text: 1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas. +library_name: torch +pipeline_tag: text-classification +--- + +Idioma: [English](../../models/decision-model-card.md) | **Español** + +# Descripción del modelo + +Este modelo es el clasificador actual de decisiones a nivel párrafo utilizado en el pipeline de producción `datapublic`. +Estima si un párrafo contiene una decisión judicial y, cuando se usa dentro del pipeline de AymurAI, emite una entidad sintética `DECISION` con una subcategoría basada en reglas. + +Este modelo fue desarrollado por [{ collective.ai }](https://collectiveai.io) como parte del proyecto [AymurAI](https://aymurai.info) de [DataGenero](https://datagenero.org). + +## Arquitectura + +El clasificador actual de producción es un modelo compacto de bolsa de palabras con hashing, implementado con `torch.nn.EmbeddingBag`. +Su camino de inferencia está definido en `aymurai/models/decision/binregex.py` y `aymurai/models/decision/embeddingbag.py`. + +A alto nivel, el modelo funciona así: + +1. El texto de entrada se normaliza quitando tildes, pasando a minúsculas y colapsando espacios. +2. El texto se tokeniza por espacios en blanco. +3. Los tokens se hashean con BLAKE2b sobre un vocabulario fijo. +4. Los IDs de tokens se agrupan mediante embeddings promedio con `EmbeddingBag`. +5. Una capa de dropout y una cabeza lineal producen logits binarios (`not decision`, `decision`). +6. Si el score positivo supera el umbral configurado, el pipeline agrega una etiqueta `DECISION` al párrafo. + +Configuración actual del checkpoint cargado por el modelo de producción: + +| parámetro | valor | +|---|---| +| `vocab_size` | `20000` | +| `embed_dim` | `64` | +| `max_tokens` | `128` | +| `dropout` | `0.1` | +| `num_classes` | `2` | +| `batch_size` | `512` | +| `lr` | `0.005` | +| `weight_decay` | `0.001` | +| `epochs` | `50` | + +## Usos previstos y limitaciones + +AymurAI está pensado como una herramienta para abordar la falta de transparencia en el sistema judicial en relación con casos de violencia de género (VG) en América Latina. El objetivo es aumentar los niveles de reporte, construir confianza en el sistema de justicia y mejorar el acceso a la justicia para mujeres y personas LGBTIQ+. AymurAI genera y mantiene datasets anonimizados a partir de sentencias judiciales para comprender la violencia de género y apoyar el diseño de políticas públicas, además de contribuir a campañas de colectivos feministas. + +Las capacidades de AymurAI se limitan a la recolección y análisis semiautomatizados de datos, y sus resultados pueden estar sujetos a limitaciones como la calidad y consistencia de los datos, posibles sesgos del modelo de IA y la disponibilidad de la información. Además, la efectividad de AymurAI para abordar la falta de transparencia del sistema judicial y mejorar el acceso a la justicia también puede depender de otros factores, como el nivel de cooperación de funcionarios judiciales y el contexto cultural y político más amplio. + +Este modelo fue entrenado con un dataset cerrado proveniente de un juzgado penal argentino. Está diseñado para identificar si un párrafo contiene una decisión judicial. El uso de un dataset específico de dominio, proveniente de un juzgado penal argentino, hace que el modelo esté ajustado al contexto jurídico y cultural concreto, lo que permite resultados más precisos. Sin embargo, esto también implica que el modelo puede no ser aplicable o efectivo en otros países o regiones con sistemas jurídicos o normas culturales diferentes. + +## Comportamiento en producción + +Cuando este clasificador se utiliza a través de `DecisionEmbeddingBagBinRegex`, el comportamiento en producción incluye dos reglas adicionales además de la predicción binaria cruda: + +- La clase positiva se emite solo cuando el score de decisión supera el umbral configurado (`0.5` en el pipeline de producción). +- Con `return_only_with_detalle=true`, el clasificador solo agrega una entidad `DECISION` si el párrafo ya contiene una entidad `DETALLE` proveniente de la etapa NER. + +Si un párrafo es clasificado como decisión, el modelo emite una entidad `DECISION` que cubre el texto completo del párrafo. La etiqueta emitida incluye una subcategoría basada en reglas: + +- `hace_lugar` +- `no_hace_lugar` + +Esa subcategoría se asigna mediante postprocesamiento con expresiones regulares sobre el texto del párrafo. + +# Uso + +## Cómo usar el modelo en torch + +```python +import torch + +from aymurai.models.decision.binregex import DecisionEmbeddingBagBinRegex + +model = DecisionEmbeddingBagBinRegex( + model_checkpoint="https://github.com/AymurAI/backend/releases/download/v2.0.0-alpha.1/tiny-embeddingbag.safetensors", + device="cpu", + threshold=0.5, + return_only_with_detalle=False, +) + +text = "1. DECLARAR EXTINGUIDA LA ACCION PENAL en este caso por cumplimiento de la suspension del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas." + +flat_tokens, offsets = model.model_input_from_text(text) +with torch.no_grad(): + logits = model.model(flat_tokens, offsets) + probabilities = logits.softmax(dim=1).cpu().numpy() + +print(probabilities) +print(model.get_subcategory(text)) +``` + +Esto produce una salida similar a: + +```text +[[0.0010057, 0.9989943]] +['hace_lugar'] +``` + +La primera columna es la probabilidad de que el texto no sea una decisión, y la segunda columna es la probabilidad de que el texto sí sea una decisión. + +## Uso del modelo en un pipeline de AymurAI + +El pipeline actual de producción `datapublic` incluye este clasificador después de la etapa NER con Flair. + +```python +from aymurai.pipeline import AymurAIPipeline + +pipeline = AymurAIPipeline.load("/resources/pipelines/production/datapublic") + +item = { + "path": "dummy", + "data": { + "doc.text": "1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas." + }, +} + +processed = pipeline.preprocess([item]) +processed = pipeline.predict_single(processed[0]) +processed = pipeline.postprocess([processed]) + +print(processed[0]["predictions"]["entities"]) +``` + +En producción, el clasificador se configura así: + +```json +{ + "aymurai.models.decision.binregex.DecisionEmbeddingBagBinRegex": { + "model_checkpoint": "https://github.com/AymurAI/backend/releases/download/v2.0.0-alpha.1/tiny-embeddingbag.safetensors", + "device": "cpu", + "threshold": 0.5, + "return_only_with_detalle": true + } +} +``` + +# Entidades y métricas + +## Descripción + +Este modelo solo considera la clasificación de párrafos como decisiones o no decisiones. +Cuando la predicción es positiva, el pipeline emite una entidad sintética `DECISION` que cubre todo el párrafo. + +Para la lista completa de entidades usadas por el flujo datapublic, consultá el catálogo de entidades de datapublic ([en](../../entities/datapublic/README.md)|[es](../entities/datapublic/README.md)). + +Para una descripción completa de las entidades consideradas por AymurAI, ver el [Glosario para el dataset con perspectiva de género](https://docs.google.com/document/d/123B9T2abCEqBaxxOl5c7HBJZRdIMtKDWo6IKHIVil04/edit), elaborado por [DataGenero](https://datagenero.org) (solo en español). + +## Datos + +El modelo fue entrenado con un dataset de 1200 resoluciones judiciales de un juzgado penal argentino. + +Dada la naturaleza de los datos (datos personales, características de la denuncia y protección de víctimas), los documentos se mantienen privados. + +### Lista de personas colaboradoras en la anotación + +El dataset fue anotado manualmente por: + +* Diego Scopetta +* Franny Rodriguez Gerzovich ([email](mailto:fraanyrodriguez@gmail.com)|[linkedin](https://www.linkedin.com/in/francescarg)) +* Laura Barreiro +* Matías Sosa +* Maximiliano Sosa +* Patricia Sandoval +* Santiago Bezchinsky ([email](mailto:santibezchinsky@gmail.com)|[linkedin](https://www.linkedin.com/in/santiago-bezchinsky)) +* Zoe Rodriguez Gerzovich + +## Métricas + +Las siguientes métricas provienen de la notebook actual de entrenamiento/evaluación de EmbeddingBag y corresponden a la tarea binaria de clasificación de decisiones (`0 = no decisión`, `1 = decisión`). + +### Split de validación + +| métrica | clase 1 (decisión) | general | +|---|---:|---:| +| precision | 0.774 | - | +| recall | 0.896 | - | +| f1-score | 0.830 | - | +| accuracy | - | 0.962 | +| support | 336 | 3211 | + +### Split de test + +| métrica | clase 1 (decisión) | general | +|---|---:|---:| +| precision | 0.754 | - | +| recall | 0.905 | - | +| f1-score | 0.823 | - | +| accuracy | - | 0.959 | +| support | 336 | 3211 | + +# Cita + +Por favor citá [el siguiente paper](https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view) al utilizar AymurAI: + +```bibtex +@techreport{feldfeber2022, + author = {Feldfeber, Ivana and Quiroga, Yasm{'i}n Bel{'e}n and Guevara, Clarissa and Ciolfi Felice, Marianela}, + title = {Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico}, + institution = {DataGenero}, + year = {2022}, + url = {https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view} +} +``` diff --git a/docs/es/models/flair-model-card.md b/docs/es/models/flair-model-card.md new file mode 100644 index 00000000..5efa7a81 --- /dev/null +++ b/docs/es/models/flair-model-card.md @@ -0,0 +1,166 @@ +--- +license: mit +language: +- es +tags: +- flair +- token-classification +- sequence-tagger-model +datasets: +- ArJuzPCyF10 +metrics: +- f1 +widget: +- text: 1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas. +library_name: flair +pipeline_tag: token-classification +--- + +Idioma: [English](../../models/flair-model-card.md) | **Español** + +# Descripción del modelo +

+schema +

+ +Siguiendo las guías de Flair para entrenar un modelo de NER, este modelo se entrenó sobre [embeddings BETO](https://huggingface.co/dccuchile/bert-base-spanish-wwm-uncased), una versión en español de BERT entrenada sobre un corpus en español, con una arquitectura BiLSTM-CRF. + +Este modelo fue desarrollado por [{ collective.ai }](https://collectiveai.io) como parte del proyecto [AymurAI](https://aymurai.info) de [DataGenero](https://datagenero.org). +Actualmente se usa como componente NER del pipeline de producción `datapublic`. + +# Usos previstos y limitaciones +AymurAI está pensado como una herramienta para abordar la falta de transparencia en el sistema judicial en relación con casos de violencia de género (VG) en América Latina. El objetivo es aumentar los niveles de reporte, construir confianza en el sistema de justicia y mejorar el acceso a la justicia para mujeres y personas LGBTIQ+. AymurAI genera y mantiene datasets anonimizados a partir de sentencias judiciales para comprender la violencia de género y apoyar el diseño de políticas públicas, además de contribuir a campañas de colectivos feministas. + +Las capacidades de AymurAI se limitan a la recolección y análisis semiautomatizados de datos, y sus resultados pueden estar sujetos a limitaciones como la calidad y consistencia de los datos, posibles sesgos del modelo de IA y la disponibilidad de la información. Además, la efectividad de AymurAI para abordar la falta de transparencia del sistema judicial y mejorar el acceso a la justicia también puede depender de otros factores, como el nivel de cooperación de funcionarios judiciales y el contexto cultural y político más amplio. + +Este modelo fue entrenado con un dataset cerrado proveniente de un juzgado penal argentino. Está diseñado para identificar y extraer información relevante de sentencias vinculadas con casos de violencia de género. El uso de un dataset específico de dominio, proveniente de un juzgado penal argentino, hace que el modelo esté ajustado al contexto jurídico y cultural concreto, lo que permite resultados más precisos. Sin embargo, esto también implica que el modelo puede no ser aplicable o efectivo en otros países o regiones con sistemas jurídicos o normas culturales diferentes. + +# Uso +## Cómo usar el modelo en Flair + +Requiere **[Flair](https://github.com/flairNLP/flair/)**. +Instalación: `pip install flair` + +```python +from flair.data import Sentence +from flair.models import SequenceTagger + +# load tagger +tagger = SequenceTagger.load("aymurai/flair-ner-spanish-judicial") + +# make example sentence +sentence = Sentence("1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas.") + +# predict NER tags +tagger.predict(sentence) + +# print sentence +print(sentence) + +# print predicted NER spans +print('The following NER tags are found:') +# iterate over entities and print +for entity in sentence.get_spans('ner'): + print(entity) +``` + +Esto produce una salida como la siguiente: + +```text +Span[2:11]: "EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento" → DETALLE (0.5498) +Span[13:18]: "suspensión del proceso a prueba" → OBJETO_DE_LA_RESOLUCION (0.5647) +Span[20:21]: "SOBRESEER" → DETALLE (0.7766) +Span[22:25]: "EZEQUIEL CAMILO MARCONNI" → NOMBRE (0.6454) +Span[35:36]: "lesiones" → CONDUCTA (0.9457) +Span[36:38]: "leves agravadas" → CONDUCTA_DESCRIPCION (0.8818) +Span[39:40]: "amenazas" → CONDUCTA (0.956) +Span[40:48]: "simples y agravadas por el uso de armas" → CONDUCTA_DESCRIPCION (0.6866) +``` + +## Uso del modelo en un pipeline de AymurAI +También podés ejecutar el modelo a través de un pipeline de AymurAI. + +```python +from aymurai.pipeline import AymurAIPipeline + +pipeline = AymurAIPipeline.load("/resources/pipelines/production/datapublic") + +item = { + 'path': 'dummy', + 'data': { + 'doc.text': "1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas." + } +} + +processed = pipeline.preprocess([item]) +processed = pipeline.predict_single(processed[0]) +processed = pipeline.postprocess([processed]) + +print(processed[0]["predictions"]["entities"]) +``` + +# Entidades y métricas +## Descripción +Consultá el catálogo de entidades de datapublic ([en](../../entities/datapublic/README.md)|[es](../entities/datapublic/README.md)). + +Para una descripción completa de las entidades consideradas por AymurAI, ver el [Glosario para el dataset con perspectiva de género](https://docs.google.com/document/d/123B9T2abCEqBaxxOl5c7HBJZRdIMtKDWo6IKHIVil04/edit), elaborado por [DataGenero](https://datagenero.org) (solo en español). + +## Datos +El modelo fue entrenado con un dataset de 1200 resoluciones judiciales de un juzgado penal argentino. + +Dada la naturaleza de los datos (datos personales, características de la denuncia y protección de víctimas), los documentos se mantienen privados. + +### Lista de personas colaboradoras en la anotación +El dataset fue anotado manualmente por: +* Diego Scopetta +* Franny Rodriguez Gerzovich ([email](mailto:fraanyrodriguez@gmail.com)|[linkedin](https://www.linkedin.com/in/francescarg)) +* Laura Barreiro +* Matías Sosa +* Maximiliano Sosa +* Patricia Sandoval +* Santiago Bezchinsky ([email](mailto:santibezchinsky@gmail.com)|[linkedin](https://www.linkedin.com/in/santiago-bezchinsky)) +* Zoe Rodriguez Gerzovich + +## Métricas + +| label | precision | recall | f1-score | +|-----------------------------------------------------|-----------|--------|----------| +| FECHA_DE_NACIMIENTO | 0.98 | 0.99 | 0.99 | +| FECHA_RESOLUCION | 0.95 | 0.98 | 0.96 | +| NACIONALIDAD | 0.94 | 0.98 | 0.96 | +| GENERO | 1.00 | 0.50 | 0.67 | +| HORA_DE_INICIO | 0.98 | 0.92 | 0.95 | +| NOMBRE | 0.94 | 0.95 | 0.95 | +| FRASES_AGRESION | 0.90 | 0.98 | 0.94 | +| HORA_DE_CIERRE | 0.90 | 0.92 | 0.91 | +| NIVEL_INSTRUCCION | 0.85 | 0.94 | 0.90 | +| N_EXPTE_EJE | 0.85 | 0.93 | 0.89 | +| TIPO_DE_RESOLUCION | 0.63 | 0.93 | 0.75 | +| VIOLENCIA_DE_GENERO | 0.49 | 0.59 | 0.54 | +| RELACION_Y_TIPO_ENTRE_ACUSADO/A_Y_DENUNCIANTE | 0.93 | 0.76 | 0.84 | +| HIJOS_HIJAS_EN_COMUN | 0.47 | 0.57 | 0.52 | +| MODALIDAD_DE_LA_VIOLENCIA | 0.57 | 0.56 | 0.57 | +| FECHA_DEL_HECHO | 0.83 | 0.83 | 0.83 | +| CONDUCTA | 0.79 | 0.67 | 0.73 | +| ART_INFRINGIDO | 0.76 | 0.74 | 0.75 | +| DETALLE | 0.53 | 0.37 | 0.43 | +| OBJETO_DE_LA_RESOLUCION | 0.60 | 0.78 | 0.68 | +| CONDUCTA_DESCRIPCION | 0.54 | 0.43 | 0.48 | +| LUGAR_DEL_HECHO | 0.75 | 0.47 | 0.58 | +| EDAD_AL_MOMENTO_DEL_HECHO | 0.50 | 0.20 | 0.29 | +| PERSONA_ACUSADA_NO_DETERMINADA | 0.71 | 0.19 | 0.30 | +| | | | | +| macro avg | 0.77 | 0.72 | 0.73 | + +# Cita +Por favor citá [el siguiente paper](https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view) al utilizar AymurAI: + +```bibtex +@techreport{feldfeber2022, + author = {Feldfeber, Ivana and Quiroga, Yasm'{\i}n Bel'{e}n and Guevara, Clarissa and Ciolfi Felice, Marianela}, + title = {Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico}, + institution = {DataGenero}, + year = {2022}, + url = {https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view} +} +``` diff --git a/docs/es/pipelines/README.md b/docs/es/pipelines/README.md new file mode 100644 index 00000000..3eb62157 --- /dev/null +++ b/docs/es/pipelines/README.md @@ -0,0 +1,25 @@ +# Pipelines +Idioma: [English](../../pipelines/README.md) | **Español** + +Esta sección documenta los pipelines de producción del backend por flujo. + +## Documentación por flujo +- Anonymizer: [anonymizer/README.md](anonymizer/README.md) +- Datapublic: [datapublic/README.md](datapublic/README.md) + +## Modelos relacionados +- Índice de modelos: [../models/README.md](../models/README.md) +- Model card de Flair NER: [../models/flair-model-card.md](../models/flair-model-card.md) +- Model card de Decision: [../models/decision-model-card.md](../models/decision-model-card.md) + +## Fuentes de configuración en producción +- Config anonymizer: `resources/pipelines/production/flair-anonymizer/pipeline.json` +- Config datapublic: `resources/pipelines/production/datapublic/pipeline.json` + +## Mapeo con API +- `POST /api/anonymizer/predict` -> `flair-anonymizer` +- `POST /api/datapublic/predict/{document_id}` -> `datapublic` + +## Documentación relacionada +- Referencia API: [../api/README.md](../api/README.md) +- Base de datos interna: [../database/README.md](../database/README.md) diff --git a/docs/es/pipelines/anonymizer/README.md b/docs/es/pipelines/anonymizer/README.md new file mode 100644 index 00000000..0c910e6d --- /dev/null +++ b/docs/es/pipelines/anonymizer/README.md @@ -0,0 +1,70 @@ +# Pipeline de Anonymizer +Idioma: [English](../../../pipelines/anonymizer/README.md) | **Español** + +Referencia técnica orientada al flujo de anonimización. + +## Alcance +Este flujo extrae entidades del texto judicial y compila documentos anonimizados. + +## Diagrama +![Diagrama del pipeline de anonymizer](../../../pipelines/anonymizer/pipeline.png) + +Fuente editable: [../../../pipelines/anonymizer/pipeline.excalidraw](../../../pipelines/anonymizer/pipeline.excalidraw) + +## Entrypoints de runtime +- `POST /api/misc/document-extract` +- `POST /api/anonymizer/predict` +- `POST /api/anonymizer/disambiguate` +- `POST /api/anonymizer/validation` +- `POST /api/anonymizer/anonymize-document` + +## Flujo paso a paso +1. Extracción de texto (`/api/misc/document-extract`) divide el documento fuente en párrafos normalizados. +2. Predicción (`/api/anonymizer/predict`) ejecuta NER por párrafo. +3. Desambiguación (`/api/anonymizer/disambiguate`) asigna IDs canónicos y metadatos efectivos para la desambiguación/anonimización. +4. Revisión manual en UI para editar etiquetas/políticas. +5. Compilación (`/api/anonymizer/anonymize-document`) aplica reemplazos preservando el formato de origen: una entrada PDF devuelve PDF y una entrada DOCX se exporta como ODT. + +## Componentes técnicos + +### Configuración del pipeline +- Fuente: `resources/pipelines/production/flair-anonymizer/pipeline.json` +- Preprocesamiento: + - `aymurai.models.flair.utils.FlairTextNormalize` +- Modelo: + - `aymurai.models.flair.core.FlairModel` + - base model path: `aymurai/anonymizer-beto-cased-flair` +- Postprocesamiento: + - `aymurai.transforms.anonymization_postprocess.core.AnonymizationEntityCleaner` + - `aymurai.transforms.datetime_formatter.core.DatetimeFormatter` + +### Contratos de API usados por este flujo +- `DocumentInformation` +- `DocumentAnnotations` +- `LabelPolicy` +- `RenderPolicy` +- `EntityAttributes` (en particular: `canonical_entity_id`, `aymurai_label_instance`, `aymurai_disambiguation`, `aymurai_anonymize`) + +### Módulos backend relevantes +- Router: `aymurai/api/endpoints/routers/anonymizer/anonymizer.py` +- Render/anonymize: `aymurai/text/anonymization/docx.py` and `aymurai/text/anonymization/pdf.py` +- Desambiguación canónica: `aymurai/utils/entity_disambiguation/` + +## Persistencia (DB) +Tablas usadas por este flujo: +- `anonymization_paragraph` +- `anonymization_document` +- `anonymization_document_paragraph` + +## Notas +- La anonimización de PDF redacta el texto en el documento y preserva el layout no afectado, las anotaciones, los metadatos y el watermark. +- La anonimización de DOCX procesa reemplazos antes de convertir el resultado a ODT. +- Las políticas por label se mergean desde entorno y request. +- `render_policy` controla el comportamiento de sufijos (`auto`, `always`, `never`). + +## Documentación relacionada +- Índice de pipelines: [../README.md](../README.md) +- Entidades de anonymizer: [../../entities/anonymizer/README.md](../../entities/anonymizer/README.md) +- Model card de anonymizer: [../../models/anonymizer-model-card.md](../../models/anonymizer-model-card.md) +- Referencia API: [../../api/README.md](../../api/README.md) +- Base interna: [../../database/README.md](../../database/README.md) diff --git a/docs/es/pipelines/datapublic/README.md b/docs/es/pipelines/datapublic/README.md new file mode 100644 index 00000000..2abdd047 --- /dev/null +++ b/docs/es/pipelines/datapublic/README.md @@ -0,0 +1,76 @@ +# Pipeline de Datapublic +Idioma: [English](../../../pipelines/datapublic/README.md) | **Español** + +Referencia técnica orientada al flujo de extracción de información del datapublic. + +## Alcance +Este flujo extrae información estructurada a partir de párrafos y soporta persistencia de validación a nivel documento para curación de dataset público. + +## Diagrama +![Diagrama del pipeline de datapublic](../../../pipelines/datapublic/pipeline.png) + +## Entrypoints de runtime +- `POST /api/misc/document-extract` +- `POST /api/datapublic/predict/{document_id}` +- `GET /api/datapublic/validation/document/{document_id}` +- `POST /api/datapublic/validation/document/{document_id}` + +## Flujo paso a paso +1. Extracción de texto (`/api/misc/document-extract`) divide el documento fuente en párrafos normalizados. +2. Predicción (`/api/datapublic/predict/{document_id}`) procesa cada párrafo y devuelve predicciones; la persistencia en caché ocurre solo cuando `use_cache=true` (default). +3. Revisión en UI agrega la salida validada a nivel documento. +4. Los endpoints de validación leen/escriben payload de validación de documento. + +## Componentes técnicos + +### Configuración del pipeline +- Fuente: `resources/pipelines/production/datapublic/pipeline.json` +- Preprocesamiento: + - `aymurai.models.flair.utils.FlairTextNormalize` +- Modelos: + - `aymurai.models.flair.core.FlairModel` (`aymurai/flair-ner-spanish-judicial`) + - `aymurai.models.decision.binregex.DecisionEmbeddingBagBinRegex` (`return_only_with_detalle=true` en producción) +- Postprocesamiento: + - `aymurai.transforms.entity_subcategories.regex.RegexSubcategorizer` + - `aymurai.transforms.datetime_formatter.core.DatetimeFormatter` + - `aymurai.transforms.entity_subcategories.sentence_transformer.SentenceTransformerSubcategorizer` (4 instancias configuradas en producción para `CONDUCTA`, `CONDUCTA_DESCRIPCION`, `DETALLE` y `OBJETO_DE_LA_RESOLUCION`) + - `aymurai.transforms.entity_subcategories.article.ArticleSubcategorizer` + +### Algoritmos y procesamiento +- Extracción NER sobre párrafos judiciales. +- Clasificación/filtro de decisiones para relevancia, con gating en producción para que `DECISION` solo se emita cuando ya existe una entidad `DETALLE`. +- Subcategorización rule-based + embeddings. +- Normalización de fecha/hora y mapeo por artículo. + +### Contratos de API usados por este flujo +- `TextRequest` +- `DocumentInformation` +- `DataPublicDocumentAnnotations` (payload libre de validación a nivel documento) + +### Módulos backend relevantes +- Router: `aymurai/api/endpoints/routers/datapublic/datapublic.py` +- Carga de pipeline e inferencia: endpoint de predicción en `aymurai/api/endpoints/routers/datapublic/datapublic.py`. + +## Persistencia (DB) +Tablas usadas por este flujo: +- `datapublic_paragraph` +- `datapublic_document` +- `datapublic_document_paragraph` + +## Notas +- El directorio de pipeline en producción sigue llamándose `datapublic`. +- `document_id` es la clave de agrupamiento a nivel documento para asociar predicciones por párrafo y el payload de validación. +- La persistencia de validación es a nivel documento y acepta intencionalmente un objeto JSON libre. +- `GET /api/datapublic/validation/document/{document_id}` devuelve `404` cuando el documento no existe; `POST` crea o actualiza el payload de validación. +- El router público no monta actualmente `/api/datapublic/dataset/*`. +- La validación a nivel párrafo aparece en código como lógica legacy comentada y no forma parte del flujo público. + +## Modelos usados por este flujo +- Flair NER: [../../models/flair-model-card.md](../../models/flair-model-card.md) +- Clasificador de decisiones: [../../models/decision-model-card.md](../../models/decision-model-card.md) + +## Documentación relacionada +- Índice de pipelines: [../README.md](../README.md) +- Entidades de datapublic: [../../entities/datapublic/README.md](../../entities/datapublic/README.md) +- Referencia API: [../../api/README.md](../../api/README.md) +- Base interna: [../../database/README.md](../../database/README.md) diff --git a/docs/metrics/Desarrollo metrica.md b/docs/metrics/Desarrollo metrica.md new file mode 100644 index 00000000..13fab9b7 --- /dev/null +++ b/docs/metrics/Desarrollo metrica.md @@ -0,0 +1,500 @@ +# Métrica de evaluación para el desambiguador de entidades + +Este documento describe la métrica con la que evaluamos el módulo de **desambiguación de entidades canónicas** en documentos judiciales. La métrica es independiente del NER (que se evalúa por separado con F1 por clase) y se centra en medir la calidad de la resolución de entidades. + +La métrica: + +- recibe dos JSON: + - `gold_json`: referencia (ground truth), + - `pred_json`: salida del desambiguador; +- calcula cuatro componentes complementarios: +- $F1_{\text{ent}}$: calidad en la **detección de entidades canónicas**, +- $\text{AliasF1}_{\text{macro}}$: calidad en la **agrupación de aliases**, +- $Acc_{\text{label}}$: exactitud del **label** (`aymurai_label`), +- $Acc_{\text{rol}}$: exactitud del **rol** (`attributes["role"]`); +- devuelve un escalar final como combinación ponderada: + +$$ +\text{Score}_{\text{global}} = +w_{\text{ent}} \cdot F1_{\text{ent}} + +w_{\text{alias}} \cdot \text{AliasF1}_{\text{macro}} + +w_{\text{label}} \cdot Acc_{\text{label}} + +w_{\text{rol}} \cdot Acc_{\text{rol}}. +$$ + +Valores por defecto de los pesos: + +- $w_{\text{ent}} = 0.40$ +- $w_{\text{alias}} = 0.35$ +- $w_{\text{label}} = 0.20$ +- $w_{\text{rol}} = 0.05$ + +--- + +## 1. Modelo de datos y notación + +Tanto `gold_json` como `pred_json` se modelan como una **lista de entidades canónicas**. Cada entidad sigue la estructura: + +```json +{ + "entity_id": "", + "aymurai_label": "", + "canonical_text": "", + "aliases": [], + "attributes": {}, + "relations": [] +} +``` + +Denotamos: + +- Conjunto de entidades gold (referencia): $G = \{ g_1, g_2, \dots, g_N \}$ +- Conjunto de entidades predichas: $P = \{ p_1, p_2, \dots, p_M \}$ + +Para cada entidad $e \in G \cup P$: + +- $\text{label}(e)$ es su `aymurai_label`. +- $\text{aliases}(e)$ es el conjunto de aliases (incluyendo `canonical_text`). +- $\text{rol}(e)$ es `attributes["role"]` si existe (en caso contrario es `None`). + +La métrica está inspirada en trabajos de **entity resolution** y **coreference**, donde interesa comparar la formación de clusters más que strings exactos. + +--- + +## 2. Preprocesamiento: normalización y conjuntos de aliases + +Cada alias se normaliza para mitigar variaciones de formato. Se procesa tanto la lista `aliases` como `canonical_text`, y se construye un conjunto de strings normalizados que representa a la entidad. + +### 2.1. Normalización de texto + +Sea una función $\text{norm}(\cdot)$ que: + +1. convierte a minúsculas, +2. aplica `strip()` para remover espacios extras, +3. elimina tildes y acentos utilizando normalización Unicode. + +```python +import unicodedata + +def normalize_text(text: str) -> str: + if text is None: + return "" + text = text.strip().lower() + text = unicodedata.normalize("NFD", text) + text = "".join(ch for ch in text if unicodedata.category(ch) != "Mn") + return text +``` + +### 2.2. Construcción del conjunto de aliases + +Para cada entidad $e$: + +$$ +\text{aliases}(e) = \{ \text{norm}(a) \mid a \in e.\text{aliases} \} \cup \{ \text{norm}(e.\text{canonical\_text}) \}. +$$ + +```python +from typing import Any, Dict, Set + +def get_alias_set(entity: Dict[str, Any]) -> Set[str]: + aliases = { + normalize_text(a) + for a in entity.get("aliases", []) + if normalize_text(a) + } + canonical = normalize_text(entity.get("canonical_text", "")) + if canonical: + aliases.add(canonical) + return aliases +``` + +--- + +## 3. Similitud entre entidades: Jaccard sobre aliases + +Para medir qué tan similares son dos entidades, usamos la similitud de **Jaccard** sobre sus conjuntos de aliases: + +$$ +\text{sim}(g_i, p_j) = +\frac{\lvert \text{aliases}(g_i) \cap \text{aliases}(p_j) \rvert} + {\lvert \text{aliases}(g_i) \cup \text{aliases}(p_j) \rvert}. +$$ + +- Valor 1: conjuntos idénticos. +- Valor 0: conjuntos disjuntos. +- Valores intermedios: superposición parcial. + +```python +from typing import Set + +def alias_jaccard(aliases_gold: Set[str], aliases_pred: Set[str]) -> float: + if not aliases_gold and not aliases_pred: + return 1.0 + union = aliases_gold | aliases_pred + if not union: + return 0.0 + inter = aliases_gold & aliases_pred + return len(inter) / len(union) +``` + +Construimos una **matriz de similitud** $S \in \mathbb{R}^{N \times M}$ donde $S_{ij} = \text{sim}(g_i, p_j)$. + +--- + +## 4. Emparejamiento de entidades (matching gold–pred) + +### 4.1. Problema + +Necesitamos decidir qué entidad predicha corresponde a cada entidad gold. Esto es un matching bipartito: + +- Si el modelo divide una entidad real en dos, solo una predicha debería emparejarse con la gold; la otra contará como falso positivo. +- Si el modelo fusiona dos entidades distintas, solo una gold se emparejará; la otra será falso negativo. + +### 4.2. Umbral de similitud + +Se define un umbral $\tau$ (por defecto $\tau = 0.3$): + +- Si $S_{ij} < \tau$, se rechaza el match. +- Si $S_{ij} \ge \tau$, el par se considera candidato. + +### 4.3. Algoritmo greedy de emparejamiento + +Usamos un matching greedy ordenado por similitud (alternativa simple al algoritmo Húngaro): + +```python +from typing import List, Tuple + +def greedy_matching( + sim_matrix: List[List[float]], + sim_threshold: float +) -> List[Tuple[int, int, float]]: + matches: List[Tuple[int, int, float]] = [] + num_gold = len(sim_matrix) + num_pred = len(sim_matrix[0]) if num_gold > 0 else 0 + + candidates = [] + for i in range(num_gold): + for j in range(num_pred): + sim = sim_matrix[i][j] + if sim >= sim_threshold: + candidates.append((sim, i, j)) + + candidates.sort(reverse=True, key=lambda x: x[0]) + + used_gold, used_pred = set(), set() + for sim, i, j in candidates: + if i in used_gold or j in used_pred: + continue + used_gold.add(i) + used_pred.add(j) + matches.append((i, j, sim)) + + return matches +``` + +El resultado es una lista de triples $(i, j, \text{sim})$ con las parejas aceptadas. + +--- + +## 5. Métrica de entidades: $F1_{\text{ent}}$ + +Sean: + +- $\text{TP}$: número de pares emparejados. +- $\text{FN} = N - \text{TP}$: entidades gold sin match (perdidas). +- $\text{FP} = M - \text{TP}$: entidades pred sin match (inventadas o splits innecesarios). + +```math +P_{\text{ent}} = \frac{\text{TP}}{\text{TP} + \text{FP}}, \qquad +R_{\text{ent}} = \frac{\text{TP}}{\text{TP} + \text{FN}} +``` + +```math +F1_{\text{ent}} = +\begin{cases} +\dfrac{2 P_{\text{ent}} R_{\text{ent}}}{P_{\text{ent}} + R_{\text{ent}}}, & \text{si } P_{\text{ent}} + R_{\text{ent}} > 0 \\ +0, & \text{en otro caso} +\end{cases} +``` + +Interpretación: penaliza entidades faltantes, inventadas y los splits/merges incorrectos. + +--- + +## 6. Métrica de aliases: $\text{AliasF1}_{\text{macro}}$ + +Para cada par emparejado $(g_i, p_j)$: + +```math +A_g = \text{aliases}(g_i), \quad +A_p = \text{aliases}(p_j), \quad +I = |A_g \cap A_p| +``` + +```math +P_{\text{alias}}^{(i)} = \frac{I}{|A_p|}, \qquad +R_{\text{alias}}^{(i)} = \frac{I}{|A_g|} +``` + +```math +F1_{\text{alias}}^{(i)} = +\begin{cases} +\dfrac{2 P_{\text{alias}}^{(i)} R_{\text{alias}}^{(i)}} + {P_{\text{alias}}^{(i)} + R_{\text{alias}}^{(i)}}, & +\text{si } P_{\text{alias}}^{(i)} + R_{\text{alias}}^{(i)} > 0 \\ +0, & \text{en otro caso} +\end{cases} +``` + +Promediamos macro sobre los matches: + +```math +\text{AliasF1}_\text{macro} = +\frac{1}{\text{TP}} \sum_{(i,j) \in M_{\text{match}}} F1_{\text{alias}}^{(i)} +``` + +- Falta de aliases relevantes → baja el recall. +- Aliases incorrectos → baja la precision. +- Splits se reflejan en menor $F1_{\text{alias}}$. + +```python +alias_f1_sum = 0.0 +for i, j, _ in matches: + A_g = gold_aliases[i] + A_p = pred_aliases[j] + inter = len(A_g & A_p) + + P_alias = inter / len(A_p) if A_p else 0.0 + R_alias = inter / len(A_g) if A_g else 0.0 + if P_alias + R_alias > 0: + F1_alias = 2 * P_alias * R_alias / (P_alias + R_alias) + else: + F1_alias = 0.0 + + alias_f1_sum += F1_alias + +AliasF1_macro = alias_f1_sum / TP if TP > 0 else 0.0 +``` + +--- + +## 7. Exactitud de label y rol + +Para cada match \((g_i, p_j)\): + +```math +\text{correctLabel}^{(i)} = +\begin{cases} +1, & \text{si } \text{label}(g_i) = \text{label}(p_j) \\ +0, & \text{en otro caso} +\end{cases} +``` + +```math +\text{correctRol}^{(i)} = +\begin{cases} +1, & \text{si } \text{rol}(g_i) = \text{rol}(p_j) \\ +0, & \text{en otro caso} +\end{cases} +``` + +```math +Acc_{\text{label}} = +\frac{1}{\text{TP}} \sum_{(i,j) \in M_{\text{match}}} \text{correctLabel}^{(i)}, \quad +Acc_{\text{rol}} = +\frac{1}{\text{TP}} \sum_{(i,j) \in M_{\text{match}}} \text{correctRol}^{(i)}. +``` + +```python +label_correct = 0 +role_correct = 0 + +for i, j, _ in matches: + if gold_labels[i] == pred_labels[j]: + label_correct += 1 + if gold_roles[i] == pred_roles[j]: + role_correct += 1 + +Acc_label = label_correct / TP if TP > 0 else 0.0 +Acc_role = role_correct / TP if TP > 0 else 0.0 +``` + +Separar la calidad de la clusterización (aliases) de la del rotulado alinea la métrica con enfoques **entity-centric** en la literatura. + +--- + +## 8. Función `compute_metrics_components` + +Integra todos los pasos anteriores y devuelve un diccionario con los cuatro componentes: + +```python +from typing import Any, Dict, List + +def compute_metrics_components( + gold_entities: List[Dict[str, Any]], + pred_entities: List[Dict[str, Any]], + sim_threshold: float = 0.3, +) -> Dict[str, float]: + if not gold_entities and not pred_entities: + return { + "F1_ent": 1.0, + "AliasF1_macro": 1.0, + "Acc_label": 1.0, + "Acc_role": 1.0, + } + + gold_aliases = [get_alias_set(e) for e in gold_entities] + pred_aliases = [get_alias_set(e) for e in pred_entities] + + gold_labels = [e.get("aymurai_label") for e in gold_entities] + pred_labels = [e.get("aymurai_label") for e in pred_entities] + + gold_roles = [e.get("attributes", {}).get("role") for e in gold_entities] + pred_roles = [e.get("attributes", {}).get("role") for e in pred_entities] + + sim_matrix = [ + [alias_jaccard(g_gold, g_pred) for g_pred in pred_aliases] + for g_gold in gold_aliases + ] + + matches = greedy_matching(sim_matrix, sim_threshold=sim_threshold) + + TP = len(matches) + FN = len(gold_entities) - TP + FP = len(pred_entities) - TP + + P_ent = TP / (TP + FP) if TP + FP > 0 else 0.0 + R_ent = TP / (TP + FN) if TP + FN > 0 else 0.0 + F1_ent = 2 * P_ent * R_ent / (P_ent + R_ent) if P_ent + R_ent > 0 else 0.0 + + alias_f1_sum = 0.0 + label_correct = 0 + role_correct = 0 + + for i, j, _ in matches: + A_g = gold_aliases[i] + A_p = pred_aliases[j] + inter = len(A_g & A_p) + + P_alias = inter / len(A_p) if A_p else 0.0 + R_alias = inter / len(A_g) if A_g else 0.0 + if P_alias + R_alias > 0: + F1_alias = 2 * P_alias * R_alias / (P_alias + R_alias) + else: + F1_alias = 0.0 + alias_f1_sum += F1_alias + + if gold_labels[i] == pred_labels[j]: + label_correct += 1 + if gold_roles[i] == pred_roles[j]: + role_correct += 1 + + AliasF1_macro = alias_f1_sum / TP if TP > 0 else 0.0 + Acc_label = label_correct / TP if TP > 0 else 0.0 + Acc_role = role_correct / TP if TP > 0 else 0.0 + + return { + "F1_ent": F1_ent, + "AliasF1_macro": AliasF1_macro, + "Acc_label": Acc_label, + "Acc_role": Acc_role, + } +``` + +--- + +## 9. Métrica escalar final: `evaluate_disambiguation` + +Función de alto nivel que combina los componentes con los pesos: + +```python +import json +from typing import Any + +def evaluate_disambiguation( + gold_json: Any, + pred_json: Any, + w_ent: float = 0.4, + w_alias: float = 0.35, + w_label: float = 0.2, + w_role: float = 0.05, + sim_threshold: float = 0.3, +) -> Tuple[float, Dict[str, float]]: + if isinstance(gold_json, str): + gold_entities = json.loads(gold_json) + else: + gold_entities = gold_json + + if isinstance(pred_json, str): + pred_entities = json.loads(pred_json) + else: + pred_entities = pred_json + + metrics = compute_metrics_components( + gold_entities, + pred_entities, + sim_threshold=sim_threshold, + ) + + return ( + w_ent * metrics["F1_ent"] + + w_alias * metrics["AliasF1_macro"] + + w_label * metrics["Acc_label"] + + w_role * metrics["Acc_role"] + ) +``` + +### Justificación de los pesos por defecto + +- **$w_{\text{ent}} = 0.40$**: prioriza detectar todas las entidades canónicas sin inventarlas. +- **$w_{\text{alias}} = 0.35$**: agrupar aliases es crucial para preservar el anonimato. +- **$w_{\text{label}} = 0.20$**: el tipo de entidad importa, pero es secundario frente a la detección. +- **$w_{\text{rol}} = 0.05$**: el rol agrega contexto, pero tiene menor peso. + +Los pesos pueden ajustarse según las necesidades del proyecto. + +--- + +## 10. Ejemplo de uso + +```python +gold = [ + { + "entity_id": "GT_1", + "aymurai_label": "PER", + "canonical_text": "Juan Pérez", + "aliases": ["Juan Pérez", "Pérez, Juan"], + "attributes": {"role": "imputado"}, + "relations": [] + } +] + +pred = [ + { + "entity_id": "P_1", + "aymurai_label": "PER", + "canonical_text": "juan perez", + "aliases": ["juan perez", "perez juan"], + "attributes": {"role": "imputado"}, + "relations": [] + } +] + +score, components = evaluate_disambiguation(gold, pred) +print("Score global:", score) +print(components) +``` + +--- + +## 11. Referencias y relación con la literatura + +- Moosavi, N. S., & Strube, M. (2016). *Which coreference evaluation metric do you trust? A proposal for a link-based entity aware metric*. ACL. +- Menestrina, D., Whang, S. E., & Garcia-Molina, H. (2010). *Evaluating entity resolution results*. PVLDB. +- Binette, O., et al. (2024). *Comprehensive evaluation frameworks for entity resolution*. + +La métrica propuesta toma conceptos de la evaluación de coreference y entity resolution y los adapta a nuestro contexto judicial: + +- priorizando no perder entidades sensibles, +- evitando crear entidades inexistentes, +- y reforzando que los aliases de una misma persona no se mezclen con los de otra. diff --git a/docs/models/README.md b/docs/models/README.md new file mode 100644 index 00000000..2d7885e9 --- /dev/null +++ b/docs/models/README.md @@ -0,0 +1,19 @@ +# Models +Language: **English** | [Español](../es/models/README.md) + +This section documents the individual models used by the backend. + +## Available model docs +- Anonymizer NER model card: [anonymizer-model-card.md](anonymizer-model-card.md) +- Flair NER: [flair-model-card.md](flair-model-card.md) +- Decision classifier: [decision-model-card.md](decision-model-card.md) + +## Current production usage +- `flair-anonymizer` uses the anonymizer NER model card documented here. +- `datapublic` uses the Flair NER model, the decision classifier, and multilingual sentence-transformer encoders for embedding-based subcategorization. + +## Related docs +- Documentation index: [../README.md](../README.md) +- Pipelines index: [../pipelines/README.md](../pipelines/README.md) +- Anonymizer flow: [../pipelines/anonymizer/README.md](../pipelines/anonymizer/README.md) +- Datapublic flow: [../pipelines/datapublic/README.md](../pipelines/datapublic/README.md) diff --git a/docs/models/anonymizer-model-card.md b/docs/models/anonymizer-model-card.md new file mode 100644 index 00000000..e50b2e69 --- /dev/null +++ b/docs/models/anonymizer-model-card.md @@ -0,0 +1,172 @@ +--- +license: mit +language: +- es +tags: +- flair +- token-classification +- sequence-tagger-model +- anonymization +- judicial-text +datasets: +- ArJuzPCyF10 +metrics: +- precision +- recall +- f1-score +widget: +- text: 1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas. +library_name: flair +pipeline_tag: token-classification +--- + +Language: **English** | [Español](../es/models/anonymizer-model-card.md) + +# Model Description + +This model is the NER component used by the production `flair-anonymizer` pipeline. +It detects spans that should be anonymized in judicial documents before disambiguation, validation, and document rendering. + +Following the Flair guidelines for NER training, the model was trained on top of [BETO embeddings](https://huggingface.co/dccuchile/bert-base-spanish-wwm-uncased), a Spanish BERT model, with a BiLSTM-CRF architecture. + +This model was developed by [{ collective.ai }](https://collectiveai.io) as part of the [AymurAI](https://aymurai.info) project by [DataGenero](https://datagenero.org). + +## Intended uses & limitations + +AymurAI is intended to help address the lack of available data on gender-based violence (GBV) rulings in Latin America. In the anonymization workflow, the immediate purpose of this model is to identify sensitive spans in legal documents so they can be reviewed and replaced before downstream use. + +AymurAI remains a domain-specific system. Its capabilities are limited to semi-automated anonymization, collection, and analysis of judicial data, and the output may be affected by annotation quality, document heterogeneity, OCR or extraction errors, and the availability of representative training data. + +This model was trained on a closed dataset from an Argentine criminal court. That domain specificity improves performance on the target setting, but it also means the model may not transfer well to other jurisdictions, document styles, or legal cultures. + +## Production behavior + +In production, this model is loaded through `aymurai.models.flair.core.FlairModel` from: + +- `resources/pipelines/production/flair-anonymizer/pipeline.json` +- model path: `aymurai/anonymizer-beto-cased-flair` + +Its raw span predictions are then post-processed by: + +- `aymurai.transforms.anonymization_postprocess.core.AnonymizationEntityCleaner` +- `aymurai.transforms.datetime_formatter.core.DatetimeFormatter` + +Those predictions feed the rest of the anonymization flow: + +1. `POST /api/anonymizer/predict` runs span extraction. +2. `POST /api/anonymizer/disambiguate` assigns canonical entity IDs and effective per-label metadata. +3. Manual review may edit labels, `label_policies`, and `render_policy`. +4. `POST /api/anonymizer/anonymize-document` applies replacements in the output document. + +# Usage + +## How to use the model in Flair + +Requires **[Flair](https://github.com/flairNLP/flair/)**. +Install it with `pip install flair`. + +```python +from flair.data import Sentence +from flair.models import SequenceTagger + +tagger = SequenceTagger.load("aymurai/anonymizer-beto-cased-flair") + +sentence = Sentence( + "1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento " + "de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO " + "MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves " + "agravadas, amenazas simples y agravadas por el uso de armas." +) + +tagger.predict(sentence) + +for entity in sentence.get_spans("ner"): + print(entity) +``` + +This yields output similar to: + +```text +Span[22:25]: "EZEQUIEL CAMILO MARCONNI" -> PER (0.9541) +Span[27:28]: "11.222.333" -> DNI (1.0) +``` + +## Using the model in an AymurAI pipeline + +```python +from aymurai.pipeline import AymurAIPipeline + +pipeline = AymurAIPipeline.load("/resources/pipelines/production/flair-anonymizer") + +item = { + "path": "dummy", + "data": { + "doc.text": ( + "Acusado: Ramiro Marrón DNI 34.555.666. " + "Fecha: 17 de noviembre de 2024." + ) + }, +} + +processed = pipeline.preprocess([item]) +processed = pipeline.predict_single(processed[0]) +processed = pipeline.postprocess([processed]) + +print(processed[0]["predictions"]["entities"]) +``` + +# Entities and metrics + +## Description + +Please refer to the anonymizer entities catalog ([en](../entities/anonymizer/README.md)|[es](../es/entities/anonymizer/README.md)). + +## Data + +The model was trained with a dataset of 535 legal rulings from an Argentine criminal court. + +Due to the nature of the data (personal data, complaint characteristics, and victim protection), the documents are kept private. + +## Metrics + +The following per-label metrics come from the published Hugging Face card for this anonymizer model family and should be interpreted as model-level NER metrics, not end-to-end anonymization quality. + +| label | precision | recall | f1-score | +|---|---:|---:|---:| +| `BANCO` | 1.00 | 0.90 | 0.95 | +| `CBU` | 0.92 | 0.92 | 0.92 | +| `CORREO_ELECTRONICO` | 1.00 | 1.00 | 1.00 | +| `CUIJ` | 1.00 | 1.00 | 1.00 | +| `CUIT_CUIL` | 1.00 | 1.00 | 1.00 | +| `DIRECCION` | 0.97 | 0.85 | 0.91 | +| `DNI` | 0.96 | 1.00 | 0.98 | +| `EDAD` | 1.00 | 0.95 | 0.97 | +| `ESTUDIOS` | 1.00 | 1.00 | 1.00 | +| `FECHA` | 1.00 | 0.99 | 1.00 | +| `LINK` | 1.00 | 0.94 | 0.97 | +| `LOC` | 0.99 | 0.72 | 0.83 | +| `MARCA_AUTOMOVIL` | 0.95 | 1.00 | 0.97 | +| `NACIONALIDAD` | 1.00 | 0.94 | 0.97 | +| `NUM_ACTUACION` | 0.84 | 0.96 | 0.90 | +| `NUM_CAJA_AHORRO` | 0.00 | 0.00 | 0.00 | +| `NUM_EXPEDIENTE` | 0.98 | 0.92 | 0.95 | +| `NUM_MATRICULA` | 0.33 | 0.50 | 0.40 | +| `PATENTE_DOMINIO` | 1.00 | 1.00 | 1.00 | +| `PER` | 0.98 | 0.97 | 0.98 | +| `TELEFONO` | 0.97 | 1.00 | 0.99 | +| `TEXTO_ANONIMIZAR` | 0.98 | 0.61 | 0.75 | +| `macro avg` | 0.91 | 0.88 | 0.89 | + +# Citation + +Please cite [the following paper](https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view) when using AymurAI: + +```bibtex +@techreport{feldfeber2022, + author = {Feldfeber, Ivana and Quiroga, Yasm{\'i}n Bel{\'e}n and Guevara, Clarissa and Ciolfi Felice, Marianela}, + title = {Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico}, + institution = {DataGenero}, + year = {2022}, + url = {https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view} +} +``` diff --git a/docs/pipeline/assets/ner-schema.png b/docs/models/assets/ner-schema.png similarity index 100% rename from docs/pipeline/assets/ner-schema.png rename to docs/models/assets/ner-schema.png diff --git a/docs/models/decision-model-card.md b/docs/models/decision-model-card.md new file mode 100644 index 00000000..715f4354 --- /dev/null +++ b/docs/models/decision-model-card.md @@ -0,0 +1,217 @@ +--- +license: mit +language: +- es +tags: +- text-classification +- embeddingbag +- binary-classification +- judicial-text +datasets: +- ArJuzPCyF10 +metrics: +- f1 +widget: +- text: 1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas. +library_name: torch +pipeline_tag: text-classification +--- + +Language: **English** | [Español](../es/models/decision-model-card.md) + +# Model Description + +This model is the current paragraph-level decision classifier used in the production `datapublic` pipeline. +It estimates whether a paragraph contains a judicial decision and, when used inside the AymurAI pipeline, emits a synthetic `DECISION` entity with a rule-based subclass. + +This model was developed by [{ collective.ai }](https://collectiveai.io) as part of the [AymurAI](https://aymurai.info) project by [DataGenero](https://datagenero.org). + +## Architecture + +The current production classifier is a compact hashed bag-of-words model implemented with `torch.nn.EmbeddingBag`. +Its inference path is defined in `aymurai/models/decision/binregex.py` and `aymurai/models/decision/embeddingbag.py`. + +At a high level, the model works as follows: + +1. Input text is normalized with accent stripping, lowercasing, and whitespace collapsing. +2. Text is tokenized by whitespace. +3. Tokens are hashed with BLAKE2b into a fixed vocabulary. +4. Token IDs are pooled with mean `EmbeddingBag` embeddings. +5. A dropout layer and a linear head produce binary logits (`not decision`, `decision`). +6. If the positive score exceeds the configured threshold, the pipeline appends a `DECISION` label to the paragraph. + +Current checkpoint configuration loaded with the production model: + +| parameter | value | +|---|---| +| `vocab_size` | `20000` | +| `embed_dim` | `64` | +| `max_tokens` | `128` | +| `dropout` | `0.1` | +| `num_classes` | `2` | +| `batch_size` | `512` | +| `lr` | `0.005` | +| `weight_decay` | `0.001` | +| `epochs` | `50` | + +## Intended uses & limitations + +AymurAI is intended to be used as a tool to address the lack of transparency in the judicial system regarding gender-based violence (GBV) cases in Latin America. The goal is to increase report levels, build trust in the justice system, and improve access to justice for women and LGBTIQ+ people. AymurAI will generate and maintain anonymized datasets from legal rulings to understand GBV and support policy making, and also contribute to feminist collectives' campaigns. + +AymurAI capabilities are limited to semi-automated data collection and analysis, and the results may be subject to limitations such as the quality and consistency of the data, potential biases in the AI model, and the availability of the data. Additionally, the effectiveness of AymurAI in addressing the lack of transparency in the judicial system and improving access to justice may also depend on other factors such as the level of cooperation from court officials and the broader cultural and political context. + +This model was trained on a closed dataset from an Argentine criminal court. It is designed to identify whether a paragraph contains a judicial decision. The use of a domain-specific dataset from an Argentine criminal court ensures that the model is tailored to the specific legal and cultural context, allowing for more accurate results. However, it also means that the model may not be applicable or effective in other countries or regions with different legal systems or cultural norms. + +## Production behavior + +When this classifier is used through `DecisionEmbeddingBagBinRegex`, the production behavior includes two additional rules beyond the raw binary prediction: + +- The positive class is emitted only when the decision score is above the configured threshold (`0.5` in the production pipeline). +- With `return_only_with_detalle=true`, the classifier only appends a `DECISION` entity if the paragraph already contains a `DETALLE` entity from the NER stage. + +If a paragraph is classified as a decision, the model emits a `DECISION` entity covering the full paragraph text. The emitted label includes a rule-based subclass: + +- `hace_lugar` +- `no_hace_lugar` + +That subclass is assigned with regex-based post-processing over the paragraph text. + +# Usage + +## How to use the model in torch + +```python +import torch + +from aymurai.models.decision.binregex import DecisionEmbeddingBagBinRegex + +model = DecisionEmbeddingBagBinRegex( + model_checkpoint="https://github.com/AymurAI/backend/releases/download/v2.0.0-alpha.1/tiny-embeddingbag.safetensors", + device="cpu", + threshold=0.5, + return_only_with_detalle=False, +) + +text = "1. DECLARAR EXTINGUIDA LA ACCION PENAL en este caso por cumplimiento de la suspension del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas." + +flat_tokens, offsets = model.model_input_from_text(text) +with torch.no_grad(): + logits = model.model(flat_tokens, offsets) + probabilities = logits.softmax(dim=1).cpu().numpy() + +print(probabilities) +print(model.get_subcategory(text)) +``` + +This yields output similar to: + +```text +[[0.0010057, 0.9989943]] +['hace_lugar'] +``` + +The first column is the probability of the text not being a decision, and the second column is the probability of the text being a decision. + +## Using the model in an AymurAI pipeline + +The current production `datapublic` pipeline includes this classifier after the Flair NER stage. + +```python +from aymurai.pipeline import AymurAIPipeline + +pipeline = AymurAIPipeline.load("/resources/pipelines/production/datapublic") + +item = { + "path": "dummy", + "data": { + "doc.text": "1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas." + }, +} + +processed = pipeline.preprocess([item]) +processed = pipeline.predict_single(processed[0]) +processed = pipeline.postprocess([processed]) + +print(processed[0]["predictions"]["entities"]) +``` + +In production, the classifier is configured as: + +```json +{ + "aymurai.models.decision.binregex.DecisionEmbeddingBagBinRegex": { + "model_checkpoint": "https://github.com/AymurAI/backend/releases/download/v2.0.0-alpha.1/tiny-embeddingbag.safetensors", + "device": "cpu", + "threshold": 0.5, + "return_only_with_detalle": true + } +} +``` + +# Entities and metrics + +## Description + +This model only considers the classification of paragraphs as decisions or non-decisions. +When the prediction is positive, the pipeline emits a synthetic `DECISION` entity that spans the full paragraph. + +For the complete list of entities used by the datapublic flow, please refer to the datapublic entities catalog ([en](../entities/datapublic/README.md)|[es](../es/entities/datapublic/README.md)). + +For a complete description of the entities considered by AymurAI, refer to the [Glossary for the Dataset with gender perspective](https://docs.google.com/document/d/123B9T2abCEqBaxxOl5c7HBJZRdIMtKDWo6IKHIVil04/edit) written by [DataGenero](https://datagenero.org) (Spanish only). + +## Data + +The model was trained with a dataset of 1200 legal rulings from an Argentine criminal court. + +Due to the nature of the data (personal data, complaint characteristics, and victim protection), the documents are kept private. + +### List of annotation contributors + +The dataset was manually annotated by: + +* Diego Scopetta +* Franny Rodriguez Gerzovich ([email](mailto:fraanyrodriguez@gmail.com)|[linkedin](https://www.linkedin.com/in/francescarg)) +* Laura Barreiro +* Matías Sosa +* Maximiliano Sosa +* Patricia Sandoval +* Santiago Bezchinsky ([email](mailto:santibezchinsky@gmail.com)|[linkedin](https://www.linkedin.com/in/santiago-bezchinsky)) +* Zoe Rodriguez Gerzovich + +## Metrics + +The following metrics were obtained from the current EmbeddingBag training/evaluation notebook and correspond to the binary decision classification task (`0 = not decision`, `1 = decision`). + +### Validation split + +| metric | class 1 (decision) | overall | +|---|---:|---:| +| precision | 0.774 | - | +| recall | 0.896 | - | +| f1-score | 0.830 | - | +| accuracy | - | 0.962 | +| support | 336 | 3211 | + +### Test split + +| metric | class 1 (decision) | overall | +|---|---:|---:| +| precision | 0.754 | - | +| recall | 0.905 | - | +| f1-score | 0.823 | - | +| accuracy | - | 0.959 | +| support | 336 | 3211 | + +# Citation + +Please cite [the following paper](https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view) when using AymurAI: + +```bibtex +@techreport{feldfeber2022, + author = {Feldfeber, Ivana and Quiroga, Yasm{\'i}n Bel{\'e}n and Guevara, Clarissa and Ciolfi Felice, Marianela}, + title = {Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico}, + institution = {DataGenero}, + year = {2022}, + url = {https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view} +} +``` diff --git a/docs/pipeline/flair-model-card.md b/docs/models/flair-model-card.md similarity index 69% rename from docs/pipeline/flair-model-card.md rename to docs/models/flair-model-card.md index 15fd493d..54143211 100644 --- a/docs/pipeline/flair-model-card.md +++ b/docs/models/flair-model-card.md @@ -16,23 +16,24 @@ library_name: flair pipeline_tag: token-classification --- +Language: **English** | [Español](../es/models/flair-model-card.md) + # Model Description

schema

-Following the FLAIR guidelines for training a NER model, we trained a model on top of [BETO embeddings](https://huggingface.co/dccuchile/bert-base-spanish-wwm-uncased) (a spanish version of BERT trained in a spanish corpus) and a BiLSTM-CRF architecture. +Following the Flair guidelines for training a NER model, we trained this model on top of [BETO embeddings](https://huggingface.co/dccuchile/bert-base-spanish-wwm-uncased), a Spanish version of BERT trained on a Spanish corpus, with a BiLSTM-CRF architecture. This model was developed by [{ collective.ai }](https://collectiveai.io) as part of the [AymurAI](https://aymurai.info) project by [DataGenero](https://datagenero.org). - - +It is currently used as the NER component of the production `datapublic` pipeline. # Intended uses & limitations AymurAI is intended to be used as a tool to address the lack of transparency in the judicial system regarding gender-based violence (GBV) cases in Latin America. The goal is to increase report levels, build trust in the justice system, and improve access to justice for women and LGBTIQ+ people. AymurAI will generate and maintain anonymized datasets from legal rulings to understand GBV and support policy making, and also contribute to feminist collectives' campaigns. -AymurAI is still a prototype and is only being implemented in Argentina and Mexico. Its capabilities are limited to semi-automated data collection and analysis, and the results may be subject to limitations such as the quality and consistency of the data, potential biases in the AI model, and the availability of the data. Additionally, the effectiveness of AymurAI in addressing the lack of transparency in the judicial system and improving access to justice may also depend on other factors such as the level of cooperation from court officials and the broader cultural and political context. +AymurAI capabilities are limited to semi-automated data collection and analysis, and the results may be subject to limitations such as the quality and consistency of the data, potential biases in the AI model, and the availability of the data. Additionally, the effectiveness of AymurAI in addressing the lack of transparency in the judicial system and improving access to justice may also depend on other factors such as the level of cooperation from court officials and the broader cultural and political context. -This model was trained with a closed dataset from an Argentine criminal court. It's is designed to identify and extract relevant information from court sentences related to GBV cases. The use of a domain specific dataset from an Argentine criminal court ensures that the model is tailored to the specific legal and cultural context, allowing for more accurate results. However, it also means that the model may not be applicable or effective in other countries or regions with different legal systems or cultural norms. +This model was trained on a closed dataset from an Argentine criminal court. It is designed to identify and extract relevant information from court sentences related to GBV cases. The use of a domain-specific dataset from an Argentine criminal court ensures that the model is tailored to the specific legal and cultural context, allowing for more accurate results. However, it also means that the model may not be applicable or effective in other countries or regions with different legal systems or cultural norms. # Usage ## How to use the model in Flair @@ -76,38 +77,13 @@ Span[39:40]: "amenazas" → CONDUCTA (0.956) Span[40:48]: "simples y agravadas por el uso de armas" → CONDUCTA_DESCRIPCION (0.6866) ``` -## Using the model in aymurai pipeline -You also can run the model directly in the aymurai pipeline. +## Using the model in an AymurAI pipeline +You can also run the model through an AymurAI pipeline. ```python from aymurai.pipeline import AymurAIPipeline -from aymurai.models.flair.utils import FlairTextNormalize -from aymurai.models.flair.core import FlairModel - -config = { - "preprocess": [ - [ - "aymurai.models.flair.utils.FlairTextNormalize", - {} - ] - ], - "models": [ - [ - "aymurai.models.flair.core.FlairModel", - { - "basepath": "https://drive.google.com/uc?id=1QJYkegv_P3d27FyRLabIUiw9154ur9-I&confirm=true", - "split_doc": false, - "device": "cpu" - } - ] - ], - "postprocess": [], - "use_cache": false -} -} - -pipeline = AymurAIPipeline.load("/resources/pipelines/production/full-paragraph") +pipeline = AymurAIPipeline.load("/resources/pipelines/production/datapublic") item = { 'path': 'dummy', @@ -116,7 +92,11 @@ item = { } } -pipeline.predict([item]) +processed = pipeline.preprocess([item]) +processed = pipeline.predict_single(processed[0]) +processed = pipeline.postprocess([processed]) + +print(processed[0]["predictions"]["entities"]) ``` @@ -124,24 +104,26 @@ pipeline.predict([item]) # Entities and metrics ## Description -Please refer to the entities' description table ([en](../data/en/entities-table.md)|[es](../data/es/entities-table.md)). +Please refer to the datapublic entities catalog ([en](../entities/datapublic/README.md)|[es](../es/entities/datapublic/README.md)). For a complete description about entities considered by AymurAI, refer to the [Glossary for the Dataset with gender perspective](https://docs.google.com/document/d/123B9T2abCEqBaxxOl5c7HBJZRdIMtKDWo6IKHIVil04/edit) written by [DataGenero](https://datagenero.org) (spanish only) ## Data The model was trained with a dataset of 1200 legal rulings from an Argentine criminal court. -Due to the nature of the data (personal data, complaint characteristics and victim protection) the documents are kept private. +Due to the nature of the data (personal data, complaint characteristics, and victim protection), the documents are kept private. + ### List of annotation contributors The dataset was manually annotated by: * Diego Scopetta -* Franny Rodriguez Gerzovich ([email](fraanyrodriguez@gmail.com)|[linkedin](https://www.linkedin.com/in/francescarg)) +* Franny Rodriguez Gerzovich ([email](mailto:fraanyrodriguez@gmail.com)|[linkedin](https://www.linkedin.com/in/francescarg)) * Laura Barreiro * Matías Sosa * Maximiliano Sosa * Patricia Sandoval -* Santiago Bezchinsky ([email](santibezchinsky@gmail.com)|[linkedin](https://www.linkedin.com/in/santiago-bezchinsky)) +* Santiago Bezchinsky ([email](mailto:santibezchinsky@gmail.com)|[linkedin](https://www.linkedin.com/in/santiago-bezchinsky)) * Zoe Rodriguez Gerzovich + ## Metrics | label | precision | recall | f1-score | @@ -179,10 +161,10 @@ Please cite [the following paper](https://drive.google.com/file/d/1P-hW0JKXWZ44F ```bibtex @techreport{feldfeber2022, - author = "Feldfeber, Ivana and Quiroga, Yasmín Belén and Guevara, Clarissa and Ciolfi Felice, Marianela", - title = "Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico", - institution = "DataGenero", - year = "2022", - url = "https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view" + author = {Feldfeber, Ivana and Quiroga, Yasm\'{\i}n Bel\'{e}n and Guevara, Clarissa and Ciolfi Felice, Marianela}, + title = {Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico}, + institution = {DataGenero}, + year = {2022}, + url = {https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view} } ``` diff --git a/docs/pipeline/README.md b/docs/pipeline/README.md deleted file mode 100644 index 22514bf3..00000000 --- a/docs/pipeline/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# Pipeline -

-schema -

- -The AymurAI pipeline is written in a modular way, each step is independent and can be replaced by another one. This allows us to easily change the models or the way in which the information is processed. -It's made up of three main steps: preprocessing, inference and postprocessing. -The preprocessing step is in charge of dividing the court rulings into paragraphs and normalizing their text. The inference step is in charge of extracting the information from the paragraphs. Finally, the postprocessing step is in charge of filtering out irrelevant decisions and formatting the text. -# Preprocessing -The flow of information in the system starts with a pre-processing step for the court rulings, where the documents are divided into paragraphs and their text is normalized. This normalization process involves the unification of multiple spaces and the removal of accents and/or special characters . -In this way, the paragraphs become the minimum unit of analysis, and predictions are generated in parallel and independently from one another. - - -# Inference -

-schema -

- -The fundamental piece of the AymurAI backend is the AI models. They are in charge of extracting information from court rulings. -For their development, we use the following work frameworks: -The NER model was developed using [Flair](https://github.com/flairNLP/flair), a Python library created by Humboldt - University of Berlin focused on natural language processing models. Flair is based on PyTorch, one of the main artificial intelligence frameworks, and allows integration with Transformers, another of the main libraries of natural language processing models. -For the decision model we use PyTorch natively. - -Each paragraph is processed by these models, and the results are combined. - -Check out the [model cards](#model-cards) for more information about the models, and the [model training](#model-training-notebooks) section for more information about how the models were trained. - -# Postprocessing -Once the inference is made, the predictions are postprocessed in order to obtain the final results. -This process involves: -* Subcategorization extraction or classification -* Filtering out irrelevant decisions -* Text formatting - -## Subcategorization -* Regex -Regular expressions allow us to identify some subcategories whose texts tend to be repeated following very marked patterns. - -* Sentence Similarity -Entities with many subcategories, and less well represented, are identified using sentence similarity. The Universal Sentence Encoder Multilingual QA model, published by Google, allows us to encode the text identified as belonging to certain categories in order to then calculate a semantic similarity score with respect to each possible subcategory and thus generate a ranking of candidates, ordered from highest to lowest similarity. - - -## Text Formating -The NER model extract non-structured information from the text. Dates and times are reformatted to a standard format. - -## Filters -Many of the decisions that are extracted by the models are not relevant to the user. Relevant decisions are those that contain a specific type of information, such as presence of other relevant entities (e.i DETALLE) - -# Models -## Model cards -* [Flair NER Spanish Judicial](./flair-model-card.md) -* [Decision Text Classification](./decision-model-card.md) - -# Model Training Notebooks -* [Flair NER Spanish Judicial](../../notebooks/experiments/ner/flair/) -* [Decision Text Classification](../../notebooks/experiments/decision/) diff --git a/docs/pipeline/assets/decision-confusion-matrix.png b/docs/pipeline/assets/decision-confusion-matrix.png deleted file mode 100644 index a88291fe..00000000 Binary files a/docs/pipeline/assets/decision-confusion-matrix.png and /dev/null differ diff --git a/docs/pipeline/assets/decision-schema.png b/docs/pipeline/assets/decision-schema.png deleted file mode 100644 index 26e4366f..00000000 Binary files a/docs/pipeline/assets/decision-schema.png and /dev/null differ diff --git a/docs/pipeline/assets/inference-example.png b/docs/pipeline/assets/inference-example.png deleted file mode 100644 index bb6000dd..00000000 Binary files a/docs/pipeline/assets/inference-example.png and /dev/null differ diff --git a/docs/pipeline/assets/postprocessing-example.png b/docs/pipeline/assets/postprocessing-example.png deleted file mode 100644 index 9bd51d84..00000000 Binary files a/docs/pipeline/assets/postprocessing-example.png and /dev/null differ diff --git a/docs/pipeline/decision-model-card.md b/docs/pipeline/decision-model-card.md deleted file mode 100644 index e1e37bb3..00000000 --- a/docs/pipeline/decision-model-card.md +++ /dev/null @@ -1,161 +0,0 @@ ---- -license: mit -language: -- es -tags: -- text-classification -datasets: -- ArJuzPCyF10 -metrics: -- f1 -widget: -- text: 1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas. -library_name: torch -pipeline_tag: text-classification ---- - -# Model Description -

-schema -

- -This model was developed by [{ collective.ai }](https://collectiveai.io) as part of the [AymurAI](https://aymurai.info) project by [DataGenero](https://datagenero.org). - - - -# Intended uses & limitations -AymurAI is intended to be used as a tool to address the lack of transparency in the judicial system regarding gender-based violence (GBV) cases in Latin America. The goal is to increase report levels, build trust in the justice system, and improve access to justice for women and LGBTIQ+ people. AymurAI will generate and maintain anonymized datasets from legal rulings to understand GBV and support policy making, and also contribute to feminist collectives' campaigns. - -AymurAI is still a prototype and is only being implemented in Argentina and Mexico. Its capabilities are limited to semi-automated data collection and analysis, and the results may be subject to limitations such as the quality and consistency of the data, potential biases in the AI model, and the availability of the data. Additionally, the effectiveness of AymurAI in addressing the lack of transparency in the judicial system and improving access to justice may also depend on other factors such as the level of cooperation from court officials and the broader cultural and political context. - -This model was trained with a closed dataset from an Argentine criminal court. It's is designed to identify and extract relevant information from court sentences related to GBV cases. The use of a domain specific dataset from an Argentine criminal court ensures that the model is tailored to the specific legal and cultural context, allowing for more accurate results. However, it also means that the model may not be applicable or effective in other countries or regions with different legal systems or cultural norms. - -# Usage -## How to use the model in torch - - -```python - -from aymurai.models.decision.binregex import DecisionConv1dBinRegex - -model = DecisionConv1dBinRegex( - tokenizer_path="https://drive.google.com/uc?id=1eljQOinpObdfBREIKxVnC5Y2g_sbhPHT&confirm=true", - model_checkpoint="https://drive.google.com/uc?id=19_YmBJnO06iS0qW8ak0zl0EIsJYin8kQ&confirm=true", - device="cpu", -) - -text = "1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas." - -input_ids = model.tokenizer.encode_batch([text]) -input_ids.shape - -# The model return the logsoftmax of the probabilities of the classes -probabilities = model.model(input_ids).exp().detach().numpy() -probabilities - - -``` - -This yields the following output: -``` -array([[2.2457261e-05, 9.9997759e-01]], dtype=float32) -``` -The first column is the probability of the text not being a decision, and the second column is the probability of the text being a decision. For this example, the model predicts that the text is a decision with a probability of 99.99%. - -## Using the model in aymurai pipeline -You also can run the model directly in the aymurai pipeline. - -```python -from aymurai.pipeline import AymurAIPipeline -from aymurai.models.flair.utils import FlairTextNormalize -from aymurai.models.decision.binregex import DecisionConv1dBinRegex - -config = { - "preprocess": [ - [ - "aymurai.models.flair.utils.FlairTextNormalize", - {} - ] - ], - "models": [ - - [ - "aymurai.models.decision.binregex.DecisionConv1dBinRegex", - { - "tokenizer_path": "https://drive.google.com/uc?id=1eljQOinpObdfBREIKxVnC5Y2g_sbhPHT&confirm=true", - "model_checkpoint": "https://drive.google.com/uc?id=19_YmBJnO06iS0qW8ak0zl0EIsJYin8kQ&confirm=true", - "device": "cpu", - "threshold": 0.5, - "return_only_with_detalle": true - } - ] - ], - "postprocess": [ - ], - "use_cache": false -} - - -pipeline = AymurAIPipeline.load("/resources/pipelines/production/full-paragraph") - -item = { - 'path': 'dummy', - 'data': { - 'doc.text': "1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas." - } -} - -pipeline.predict([item]) - - -``` - - - - -# Entities and metrics -## Description -This model only considers the classification of paragraphs being decisions or not. - -For the complete list of entities, please refer to the entities' description table ([en](../data/en/entities-table.md)|[es](../data/es/entities-table.md)). - -For a complete description about entities considered by AymurAI, refer to the [Glossary for the Dataset with gender perspective](https://docs.google.com/document/d/123B9T2abCEqBaxxOl5c7HBJZRdIMtKDWo6IKHIVil04/edit) written by [DataGenero](https://datagenero.org) (spanish only) - - -## Data -The model was trained with a dataset of 1200 legal rulings from an Argentine criminal court. - -Due to the nature of the data (personal data, complaint characteristics and victim protection) the documents are kept private. -### List of annotation contributors -The dataset was manually annotated by: -* Diego Scopetta -* Franny Rodriguez Gerzovich ([email](fraanyrodriguez@gmail.com)|[linkedin](https://www.linkedin.com/in/francescarg)) -* Laura Barreiro -* Matías Sosa -* Maximiliano Sosa -* Patricia Sandoval -* Santiago Bezchinsky ([email](santibezchinsky@gmail.com)|[linkedin](https://www.linkedin.com/in/santiago-bezchinsky)) -* Zoe Rodriguez Gerzovich -## Metrics - -

-schema -

- -Class 0: Not a decision -Class 1: Decision - - - -## Citation -Please cite [the following paper](https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view) when using AymurAI: - -```bibtex -@techreport{feldfeber2022, - author = "Feldfeber, Ivana and Quiroga, Yasmín Belén and Guevara, Clarissa and Ciolfi Felice, Marianela", - title = "Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico", - institution = "DataGenero", - year = "2022", - url = "https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view" -} -``` diff --git a/docs/pipelines/README.md b/docs/pipelines/README.md new file mode 100644 index 00000000..a2b2605a --- /dev/null +++ b/docs/pipelines/README.md @@ -0,0 +1,25 @@ +# Pipelines +Language: **English** | [Español](../es/pipelines/README.md) + +This section documents AymurAI backend production pipelines by workflow. + +## Flow docs +- Anonymizer: [anonymizer/README.md](anonymizer/README.md) +- Datapublic: [datapublic/README.md](datapublic/README.md) + +## Related model docs +- Models index: [../models/README.md](../models/README.md) +- Flair NER model card: [../models/flair-model-card.md](../models/flair-model-card.md) +- Decision model card: [../models/decision-model-card.md](../models/decision-model-card.md) + +## Production pipeline sources +- Anonymizer config: `resources/pipelines/production/flair-anonymizer/pipeline.json` +- Datapublic config: `resources/pipelines/production/datapublic/pipeline.json` + +## API mapping +- `POST /api/anonymizer/predict` -> `flair-anonymizer` +- `POST /api/datapublic/predict/{document_id}` -> `datapublic` + +## Related docs +- API reference: [../api/README.md](../api/README.md) +- Internal database: [../database/README.md](../database/README.md) diff --git a/docs/pipelines/anonymizer/README.md b/docs/pipelines/anonymizer/README.md new file mode 100644 index 00000000..5b724b05 --- /dev/null +++ b/docs/pipelines/anonymizer/README.md @@ -0,0 +1,70 @@ +# Anonymizer Pipeline +Language: **English** | [Español](../../es/pipelines/anonymizer/README.md) + +Workflow-oriented technical reference for anonymization. + +## Scope +This flow extracts entities from judicial text and compiles anonymized output documents. + +## Diagram +![Anonymizer pipeline diagram](pipeline.png) + +Editable source: [pipeline.excalidraw](pipeline.excalidraw) + +## Runtime entrypoints +- `POST /api/misc/document-extract` +- `POST /api/anonymizer/predict` +- `POST /api/anonymizer/disambiguate` +- `POST /api/anonymizer/validation` +- `POST /api/anonymizer/anonymize-document` + +## Step-by-step flow +1. Text extraction (`/api/misc/document-extract`) splits source document into normalized paragraphs. +2. Prediction (`/api/anonymizer/predict`) runs NER on each paragraph. +3. Disambiguation (`/api/anonymizer/disambiguate`) assigns canonical entity IDs and effective anonymization/disambiguation metadata. +4. Manual review in UI edits labels and optional policies. +5. Compilation (`/api/anonymizer/anonymize-document`) applies replacements while preserving the source format: PDF input returns PDF, while DOCX input is exported as ODT. + +## Technical components + +### Pipeline configuration +- Source: `resources/pipelines/production/flair-anonymizer/pipeline.json` +- Preprocess: + - `aymurai.models.flair.utils.FlairTextNormalize` +- Model: + - `aymurai.models.flair.core.FlairModel` + - base model path: `aymurai/anonymizer-beto-cased-flair` +- Postprocess: + - `aymurai.transforms.anonymization_postprocess.core.AnonymizationEntityCleaner` + - `aymurai.transforms.datetime_formatter.core.DatetimeFormatter` + +### API contracts used by this flow +- `DocumentInformation` +- `DocumentAnnotations` +- `LabelPolicy` +- `RenderPolicy` +- `EntityAttributes` (notably: `canonical_entity_id`, `aymurai_label_instance`, `aymurai_disambiguation`, `aymurai_anonymize`) + +### Core backend modules +- Router: `aymurai/api/endpoints/routers/anonymizer/anonymizer.py` +- Rendering: `aymurai/text/anonymization/docx.py` and `aymurai/text/anonymization/pdf.py` +- Canonical entity mapping: `aymurai/utils/entity_disambiguation/` + +## Persistence (DB) +Tables touched by this flow: +- `anonymization_paragraph` +- `anonymization_document` +- `anonymization_document_paragraph` + +## Notes +- PDF anonymization redacts text in place and preserves unaffected layout, annotations, metadata, and watermark behavior. +- DOCX anonymization handles replacements before conversion to ODT. +- Label policies are merged from environment and request payload. +- Render policy controls suffix behavior (`auto`, `always`, `never`) during replacement. + +## Related docs +- Pipelines index: [../README.md](../README.md) +- Anonymizer entities: [../../entities/anonymizer/README.md](../../entities/anonymizer/README.md) +- Anonymizer model card: [../../models/anonymizer-model-card.md](../../models/anonymizer-model-card.md) +- API reference: [../../api/README.md](../../api/README.md) +- Internal database: [../../database/README.md](../../database/README.md) diff --git a/docs/pipelines/anonymizer/pipeline.excalidraw b/docs/pipelines/anonymizer/pipeline.excalidraw new file mode 100644 index 00000000..f7da8a73 --- /dev/null +++ b/docs/pipelines/anonymizer/pipeline.excalidraw @@ -0,0 +1,1748 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", + "elements": [ + { + "id": "M3_9cSeevpvgvuGOJmtLu", + "type": "rectangle", + "x": 341.0753968253973, + "y": -946.5918028903099, + "width": 215.66666666666663, + "height": 280.0447761194029, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0P", + "roundness": { + "type": 3 + }, + "seed": 630737269, + "version": 231, + "versionNonce": 857423573, + "isDeleted": false, + "boundElements": [ + { + "id": "EOtxLYz_c5dBLBZTsf6bP", + "type": "arrow" + } + ], + "updated": 1764875361089, + "link": null, + "locked": false + }, + { + "id": "e0VYlO1HBUE6Y61hMmjQS", + "type": "line", + "x": 384.5306207059946, + "y": -906.3554844823498, + "width": 94.95771144278605, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0Q", + "roundness": { + "type": 2 + }, + "seed": 924406485, + "version": 197, + "versionNonce": 1258245525, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 94.95771144278605, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "zz4q6sGFYLSQEGm4_Z_pZ", + "type": "line", + "x": 380.15053275977357, + "y": -879.2079349266717, + "width": 48.28358208955214, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0R", + "roundness": { + "type": 2 + }, + "seed": 2054303797, + "version": 300, + "versionNonce": 2102055669, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 48.28358208955214, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "S9t8FU85SfKbOpC8RR_gM", + "type": "line", + "x": 380.6970945304854, + "y": -849.4353562329334, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0S", + "roundness": { + "type": 2 + }, + "seed": 227041685, + "version": 330, + "versionNonce": 1522445397, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "DL-iPq6d9xxkjVSnghr83", + "type": "text", + "x": 394.20929106726203, + "y": -619.8728974176731, + "width": 82.81063842773438, + "height": 120.7089552238806, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0T", + "roundness": null, + "seed": 898721525, + "version": 217, + "versionNonce": 1900882722, + "isDeleted": false, + "boundElements": [], + "updated": 1772204466428, + "link": null, + "locked": false, + "text": ".docx\n.pdf\n.odt", + "fontSize": 32.189054726368155, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": ".docx\n.pdf\n.odt", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "eb7-zegRUYIJqYobn0GTg", + "type": "line", + "x": 379.7022624970393, + "y": -821.0544894574741, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0U", + "roundness": { + "type": 2 + }, + "seed": 1964163157, + "version": 228, + "versionNonce": 2143640341, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "76_13jw7FNEf9CD3vN4K7", + "type": "line", + "x": 386.86680332119363, + "y": -794.1556155836837, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0V", + "roundness": { + "type": 2 + }, + "seed": 1642196405, + "version": 262, + "versionNonce": 418590837, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "7bpvHSILjOEGoxBVuVvmL", + "type": "line", + "x": 387.21934633300566, + "y": -768.6496839272999, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0W", + "roundness": { + "type": 2 + }, + "seed": 992243477, + "version": 368, + "versionNonce": 1820285397, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "hunvhsGsscndVNFWE5PbJ", + "type": "line", + "x": 386.2245142995596, + "y": -740.2688171518405, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0X", + "roundness": { + "type": 2 + }, + "seed": 1062192245, + "version": 266, + "versionNonce": 1132418869, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "T8wwR2IwhMmplgY6InC65", + "type": "line", + "x": 393.3890551237139, + "y": -713.3699432780502, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0Y", + "roundness": { + "type": 2 + }, + "seed": 1110011349, + "version": 300, + "versionNonce": 676898965, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "EOtxLYz_c5dBLBZTsf6bP", + "type": "arrow", + "x": 560.3079384765334, + "y": -801.8590832623923, + "width": 311.10872819013355, + "height": 3.3450992498784444, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0Z", + "roundness": { + "type": 2 + }, + "seed": 46590773, + "version": 868, + "versionNonce": 416388706, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "AQOsTPNecUVIP5dcYG-CL" + } + ], + "updated": 1772207740751, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 311.10872819013355, + 3.3450992498784444 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "M3_9cSeevpvgvuGOJmtLu", + "focus": 0.024877658056798795, + "gap": 5.80129262390335 + }, + "endBinding": { + "elementId": "vWBP3oXNVHZGwRqV71dye", + "focus": 0.16647678467610177, + "gap": 26.273809523809405 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "AQOsTPNecUVIP5dcYG-CL", + "type": "text", + "x": 633.3624170125181, + "y": -825.1557337445369, + "width": 164.99977111816406, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0a", + "roundness": null, + "seed": 1099807893, + "version": 58, + "versionNonce": 587560482, + "isDeleted": false, + "boundElements": [], + "updated": 1772204827339, + "link": null, + "locked": false, + "text": "/misc/document-\nextract", + "fontSize": 20, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "EOtxLYz_c5dBLBZTsf6bP", + "originalText": "/misc/document-extract", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "vWBP3oXNVHZGwRqV71dye", + "type": "text", + "x": 897.6904761904764, + "y": -891.8596681096677, + "width": 536.5656565656566, + "height": 232.7272727272728, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0b", + "roundness": null, + "seed": 372466165, + "version": 440, + "versionNonce": 1496857790, + "isDeleted": false, + "boundElements": [ + { + "id": "EOtxLYz_c5dBLBZTsf6bP", + "type": "arrow" + } + ], + "updated": 1772204827339, + "link": null, + "locked": false, + "text": "{\n \"document\": [\n \"R., MATIAS EZEQUIEL SOBRE\n128 1 párr\",\n ...\n ],\n \"document_id\": \n}", + "fontSize": 23.27272727272728, + "fontFamily": 8, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "{\n \"document\": [\n \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n ...\n ],\n \"document_id\": \n}", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "id": "uIFK9kOYNJE4xD48qvRxR", + "type": "text", + "x": 1973.8949799899103, + "y": -527.8682896843161, + "width": 351.6396893419871, + "height": 74.26086756682336, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0f", + "roundness": null, + "seed": 1570520949, + "version": 508, + "versionNonce": 1517826942, + "isDeleted": false, + "boundElements": [], + "updated": 1772207749195, + "link": null, + "locked": false, + "text": "Per-paragraph prediction\n(DocumentInformation)", + "fontSize": 29.704347026729355, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Per-paragraph prediction\n(DocumentInformation)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "cPvNsqjYFBYhf1dvqCfUM", + "type": "text", + "x": 5111.82726940021, + "y": -661.9813153131714, + "width": 335.071044921875, + "height": 80.4726368159204, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0o", + "roundness": null, + "seed": 183537621, + "version": 526, + "versionNonce": 1023679678, + "isDeleted": false, + "boundElements": [], + "updated": 1772207001336, + "link": null, + "locked": false, + "text": "Anonymized document\n.odt", + "fontSize": 32.189054726368155, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Anonymized document\n.odt", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "H45OT-nnyrYBIdX_fHnB-", + "type": "rectangle", + "x": 5171.529458527813, + "y": -988.7002207858083, + "width": 215.66666666666663, + "height": 280.0447761194029, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mnNZnb2eX5SBS_s6ArNSy" + ], + "frameId": null, + "index": "b0o8", + "roundness": { + "type": 3 + }, + "seed": 1543168597, + "version": 491, + "versionNonce": 1377686782, + "isDeleted": false, + "boundElements": [], + "updated": 1772207001336, + "link": null, + "locked": false + }, + { + "id": "HkLFBBIeoT2NmdHolQogn", + "type": "line", + "x": 5214.984682408411, + "y": -948.4639023778478, + "width": 94.95771144278605, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mnNZnb2eX5SBS_s6ArNSy" + ], + "frameId": null, + "index": "b0oG", + "roundness": { + "type": 2 + }, + "seed": 997576629, + "version": 455, + "versionNonce": 1138132286, + "isDeleted": false, + "boundElements": [], + "updated": 1772207001336, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 94.95771144278605, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "dP23wQQAJGa8EbTko6TLk", + "type": "line", + "x": 5210.604594462189, + "y": -921.31635282217, + "width": 48.28358208955214, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mnNZnb2eX5SBS_s6ArNSy" + ], + "frameId": null, + "index": "b0oV", + "roundness": { + "type": 2 + }, + "seed": 1005326613, + "version": 558, + "versionNonce": 328150398, + "isDeleted": false, + "boundElements": [], + "updated": 1772207001336, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 48.28358208955214, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "AzAxanw4nR0bo9R-OtYUW", + "type": "line", + "x": 5211.1511562329015, + "y": -891.5437741284319, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mnNZnb2eX5SBS_s6ArNSy" + ], + "frameId": null, + "index": "b0ol", + "roundness": { + "type": 2 + }, + "seed": 1255351925, + "version": 588, + "versionNonce": 1854459326, + "isDeleted": false, + "boundElements": [], + "updated": 1772207001336, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "FZMsE2ftxJlqlXy5y1Kot", + "type": "line", + "x": 5210.156324199455, + "y": -863.1629073529726, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mnNZnb2eX5SBS_s6ArNSy" + ], + "frameId": null, + "index": "b0p", + "roundness": { + "type": 2 + }, + "seed": 710528309, + "version": 485, + "versionNonce": 837051902, + "isDeleted": false, + "boundElements": [], + "updated": 1772207001336, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "c5LayQF2_U6-IqmzQigBO", + "type": "line", + "x": 5217.320865023609, + "y": -836.2640334791819, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mnNZnb2eX5SBS_s6ArNSy" + ], + "frameId": null, + "index": "b0pG", + "roundness": { + "type": 2 + }, + "seed": 2020892309, + "version": 520, + "versionNonce": 1856173630, + "isDeleted": false, + "boundElements": [], + "updated": 1772207001336, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "CZP0a-w2xzB-aIeBIdTfj", + "type": "line", + "x": 5217.673408035422, + "y": -810.758101822798, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mnNZnb2eX5SBS_s6ArNSy" + ], + "frameId": null, + "index": "b0pV", + "roundness": { + "type": 2 + }, + "seed": 1005962229, + "version": 626, + "versionNonce": 1839562366, + "isDeleted": false, + "boundElements": [], + "updated": 1772207001336, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "YGpIXuZRgXAXrbeyTS72v", + "type": "line", + "x": 5216.678576001976, + "y": -782.3772350473388, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mnNZnb2eX5SBS_s6ArNSy" + ], + "frameId": null, + "index": "b0q", + "roundness": { + "type": 2 + }, + "seed": 1222868309, + "version": 524, + "versionNonce": 866975422, + "isDeleted": false, + "boundElements": [], + "updated": 1772207001336, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "ABZSKgAIt_rm_gemO06m9", + "type": "line", + "x": 5223.8431168261295, + "y": -755.4783611735489, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mnNZnb2eX5SBS_s6ArNSy" + ], + "frameId": null, + "index": "b0r", + "roundness": { + "type": 2 + }, + "seed": 1452477109, + "version": 558, + "versionNonce": 89814782, + "isDeleted": false, + "boundElements": [], + "updated": 1772207001336, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "hotXOq7-8juh39kLJem4p", + "type": "arrow", + "x": 2454.591377430666, + "y": -806.9942535766841, + "width": 374.23286489749944, + "height": 2.3331459911660204, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0u", + "roundness": { + "type": 2 + }, + "seed": 755633851, + "version": 550, + "versionNonce": 1019750946, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "XSjKuWf82l8iev5nfszlU" + } + ], + "updated": 1772206699138, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 374.23286489749944, + 2.3331459911660204 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "XSjKuWf82l8iev5nfszlU", + "type": "text", + "x": 2517.6379398842982, + "y": -818.327680581101, + "width": 248.13973999023438, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0uV", + "roundness": null, + "seed": 193864190, + "version": 43, + "versionNonce": 360400482, + "isDeleted": false, + "boundElements": null, + "updated": 1772206699138, + "link": null, + "locked": false, + "text": "/anonymizer/disambiguate", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "hotXOq7-8juh39kLJem4p", + "originalText": "/anonymizer/disambiguate", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "gXsFYxTEKv14jMJGLJI8O", + "type": "arrow", + "x": 1452.931012958739, + "y": -798.134476023053, + "width": 403.8000183105464, + "height": 5.908527289127619, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0v", + "roundness": { + "type": 2 + }, + "seed": 1974610101, + "version": 1591, + "versionNonce": 620553314, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "Q4IQFcmT8i1SnTx5DuzTS" + } + ], + "updated": 1772205508184, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 195.91698365823413, + -2.048276293639333 + ], + [ + 403.8000183105464, + -5.908527289127619 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "Q4IQFcmT8i1SnTx5DuzTS", + "type": "text", + "x": 1547.6477860456841, + "y": -810.1827523166924, + "width": 202.40042114257812, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0w", + "roundness": null, + "seed": 2134198805, + "version": 73, + "versionNonce": 1627097790, + "isDeleted": false, + "boundElements": [], + "updated": 1772205508184, + "link": null, + "locked": false, + "text": "N x /anonymizer/predict", + "fontSize": 16, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "gXsFYxTEKv14jMJGLJI8O", + "originalText": "N x /anonymizer/predict", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "jFnC8PtgWHvjc9weWuFWX", + "type": "text", + "x": 326.69940776522253, + "y": -1119.4404761904757, + "width": 558.84033203125, + "height": 76.99999999999986, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0z", + "roundness": null, + "seed": 29900859, + "version": 88, + "versionNonce": 181080958, + "isDeleted": false, + "boundElements": [], + "updated": 1772204432143, + "link": null, + "locked": false, + "text": "Anonymizer pipeline", + "fontSize": 61.59999999999989, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Anonymizer pipeline", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "LUVMyylE6hgpMsgfw7aao", + "type": "rectangle", + "x": 1888.8814913275708, + "y": -991.508812100596, + "width": 521.6666666666664, + "height": 428.88888888888886, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b13", + "roundness": { + "type": 3 + }, + "seed": 1127296034, + "version": 420, + "versionNonce": 1975633314, + "isDeleted": false, + "boundElements": [], + "updated": 1772204924667, + "link": null, + "locked": false + }, + { + "id": "1ulzr8Q7P6FFgdWlFr0cp", + "type": "text", + "x": 1911.6383476101225, + "y": -918.7310343228181, + "width": 476.1529541015625, + "height": 283.3333333333333, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b14", + "roundness": null, + "seed": 238770146, + "version": 239, + "versionNonce": 7850686, + "isDeleted": false, + "boundElements": [], + "updated": 1772206121905, + "link": null, + "locked": false, + "text": "{\n \"document\": \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n \"labels\": [\n {\n \"text\": \"R., MATIAS EZEQUIEL\",\n \"start_char\": 0,\n \"end_char\": 23,\n \"attrs\": {\n \"aymurai_label\": \"PER\",\n ...\n }\n }\n ]\n}", + "fontSize": 16.19047619047619, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "{\n \"document\": \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n \"labels\": [\n {\n \"text\": \"R., MATIAS EZEQUIEL\",\n \"start_char\": 0,\n \"end_char\": 23,\n \"attrs\": {\n \"aymurai_label\": \"PER\",\n ...\n }\n }\n ]\n}", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "vrSceZivuOFwwm43eU0mr", + "type": "text", + "x": 924.6722658756239, + "y": -606.9260633331838, + "width": 482.6020771953618, + "height": 43.38079023612419, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b15", + "roundness": null, + "seed": 1891961378, + "version": 472, + "versionNonce": 550795810, + "isDeleted": false, + "boundElements": [], + "updated": 1772207740751, + "link": null, + "locked": false, + "text": "Document extraction output", + "fontSize": 34.70463218889935, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Document extraction output", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "GNTtv7Qdsx0aEF6kxRZ5a", + "type": "text", + "x": 2736.8880249454437, + "y": -1160.814903248222, + "width": 1100.7685546875, + "height": 832.0238095238095, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b17", + "roundness": null, + "seed": 882291582, + "version": 555, + "versionNonce": 362426494, + "isDeleted": false, + "boundElements": [], + "updated": 1772207723129, + "link": null, + "locked": false, + "text": "{\n \"data\": [\n {\n \"document\": \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n \"labels\": [\n {\n \"text\": \"R., MATIAS EZEQUIEL\",\n \"start_char\": 0,\n \"end_char\": 23,\n \"attrs\": {\n \"aymurai_label\": \"PER\",\n \"aymurai_label_instance\": 1,\n 'aymurai_disambiguation': 'fuzzy',\n 'aymurai_anonymize': True,\n 'canonical_entity_id': \n }\n }\n ]\n },\n ...\n ],\n \"label_policies\": {\n \"PER\": {\"anonymize\": True, \"disambiguation\": \"fuzzy\", \"use_subclass_when_available\": True},\n \"FECHA\": {\"anonymize\": False, \"disambiguation\": \"none\", \"use_subclass_when_available\": False}\n },\n \"render_policy\": {\n \"suffix_mode\": \"auto\",\n \"suffix_threshold\": 1\n }", + "fontSize": 22.952380952380953, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "{\n \"data\": [\n {\n \"document\": \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n \"labels\": [\n {\n \"text\": \"R., MATIAS EZEQUIEL\",\n \"start_char\": 0,\n \"end_char\": 23,\n \"attrs\": {\n \"aymurai_label\": \"PER\",\n \"aymurai_label_instance\": 1,\n 'aymurai_disambiguation': 'fuzzy',\n 'aymurai_anonymize': True,\n 'canonical_entity_id': \n }\n }\n ]\n },\n ...\n ],\n \"label_policies\": {\n \"PER\": {\"anonymize\": True, \"disambiguation\": \"fuzzy\", \"use_subclass_when_available\": True},\n \"FECHA\": {\"anonymize\": False, \"disambiguation\": \"none\", \"use_subclass_when_available\": False}\n },\n \"render_policy\": {\n \"suffix_mode\": \"auto\",\n \"suffix_threshold\": 1\n }", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "GpgRoWvkFU8pY8qyanTEz", + "type": "text", + "x": 2971.7865914541294, + "y": -294.843926983973, + "width": 630.971421670128, + "height": 41.08319145660856, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b18", + "roundness": null, + "seed": 440634338, + "version": 526, + "versionNonce": 60141218, + "isDeleted": false, + "boundElements": [], + "updated": 1772207760618, + "link": null, + "locked": false, + "text": "Review payload (DocumentAnnotations)", + "fontSize": 32.86655316528685, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Review payload (DocumentAnnotations)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t1CVOK0LIbIN3hNMusjFo", + "type": "arrow", + "x": 3486.838089935834, + "y": -810.7442535766841, + "width": 374.23286489749944, + "height": 2.3331459911660204, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b19", + "roundness": { + "type": 2 + }, + "seed": 1603482402, + "version": 639, + "versionNonce": 2029670818, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "9SlkoqV3LCMNC6VOzAR4J" + } + ], + "updated": 1772206766396, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 374.23286489749944, + 2.3331459911660204 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "9SlkoqV3LCMNC6VOzAR4J", + "type": "text", + "x": 3609.451244288067, + "y": -828.7443472477678, + "width": 122.33988952636719, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1A", + "roundness": null, + "seed": 2126806754, + "version": 83, + "versionNonce": 1522294242, + "isDeleted": false, + "boundElements": [], + "updated": 1772206766396, + "link": null, + "locked": false, + "text": "Review in UI", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "t1CVOK0LIbIN3hNMusjFo", + "originalText": "Review in UI", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "1g5mwsMmOsivD02csjkTB", + "type": "text", + "x": 3879.9078976523183, + "y": -604.1107637238993, + "width": 809.6706988450118, + "height": 36.97264967907313, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1C", + "roundness": null, + "seed": 306784958, + "version": 693, + "versionNonce": 1588957730, + "isDeleted": false, + "boundElements": [], + "updated": 1772207711810, + "link": null, + "locked": false, + "text": "Manual review: edit labels, label policies and render policy", + "fontSize": 29.5781197432585, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Manual review: edit labels, label policies and render policy", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "lhZO6MPRRTWy3V55I0vZM", + "type": "arrow", + "x": 4705.8560612283145, + "y": -807.5294042874617, + "width": 403.8000183105464, + "height": 5.908527289127619, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1G", + "roundness": { + "type": 2 + }, + "seed": 280036245, + "version": 1662, + "versionNonce": 1583584226, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "GHolMdYEXJBOSjiLEJF7i" + } + ], + "updated": 1772207526341, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 195.91698365823413, + -2.048276293639333 + ], + [ + 403.8000183105464, + -5.908527289127619 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "GHolMdYEXJBOSjiLEJF7i", + "type": "text", + "x": 4769.7727702283455, + "y": -819.577680581101, + "width": 264.00054931640625, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1H", + "roundness": null, + "seed": 1331022069, + "version": 58, + "versionNonce": 451123582, + "isDeleted": false, + "boundElements": [], + "updated": 1772206985239, + "link": null, + "locked": false, + "text": "/anonymizer/anonymize-document", + "fontSize": 16, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "lhZO6MPRRTWy3V55I0vZM", + "originalText": "/anonymizer/anonymize-document", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "oRmHjBYwEzoCu3wVivfxA", + "type": "image", + "x": 3870.418043822792, + "y": -974.1722733335696, + "width": 828.6504065040658, + "height": 351.8703703703709, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1M", + "roundness": null, + "seed": 22438654, + "version": 258, + "versionNonce": 1815649534, + "isDeleted": false, + "boundElements": [], + "updated": 1772207295559, + "link": null, + "locked": false, + "status": "pending", + "fileId": "ac6c81549182e851ebe5aa58dd0fba44c4ab568b", + "scale": [ + 1, + 1 + ], + "crop": { + "x": 0, + "y": 0, + "width": 1440, + "height": 611.4681527412586, + "naturalWidth": 1440, + "naturalHeight": 820 + } + } + ], + "appState": { + "gridSize": 20, + "gridStep": 5, + "gridModeEnabled": false, + "viewBackgroundColor": "#ffffff" + }, + "files": { + "ac6c81549182e851ebe5aa58dd0fba44c4ab568b": { + "mimeType": "image/png", + "id": "ac6c81549182e851ebe5aa58dd0fba44c4ab568b", + "dataURL": "data:image/png;base64,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", + "created": 1772207236966, + "lastRetrieved": 1772207236966 + } + } +} \ No newline at end of file diff --git a/docs/pipelines/anonymizer/pipeline.png b/docs/pipelines/anonymizer/pipeline.png new file mode 100644 index 00000000..61ce789e Binary files /dev/null and b/docs/pipelines/anonymizer/pipeline.png differ diff --git a/docs/pipelines/datapublic/README.md b/docs/pipelines/datapublic/README.md new file mode 100644 index 00000000..0bcadad2 --- /dev/null +++ b/docs/pipelines/datapublic/README.md @@ -0,0 +1,76 @@ +# Datapublic Pipeline +Language: **English** | [Español](../../es/pipelines/datapublic/README.md) + +Workflow-oriented technical reference for the datapublic information extraction flow. + +## Scope +This flow extracts structured information from paragraphs and supports document-level validation persistence for public dataset curation. + +## Diagram +![Datapublic pipeline diagram](pipeline.png) + +## Runtime entrypoints +- `POST /api/misc/document-extract` +- `POST /api/datapublic/predict/{document_id}` +- `GET /api/datapublic/validation/document/{document_id}` +- `POST /api/datapublic/validation/document/{document_id}` + +## Step-by-step flow +1. Text extraction (`/api/misc/document-extract`) splits source document into normalized paragraphs. +2. Prediction (`/api/datapublic/predict/{document_id}`) processes each paragraph and returns predictions; cache persistence happens only when `use_cache=true` (default). +3. UI review aggregates document-level validated output. +4. Validation read/write endpoints persist and retrieve document-level validation payload. + +## Technical components + +### Pipeline configuration +- Source: `resources/pipelines/production/datapublic/pipeline.json` +- Preprocess: + - `aymurai.models.flair.utils.FlairTextNormalize` +- Models: + - `aymurai.models.flair.core.FlairModel` (`aymurai/flair-ner-spanish-judicial`) + - `aymurai.models.decision.binregex.DecisionEmbeddingBagBinRegex` (`return_only_with_detalle=true` in production) +- Postprocess: + - `aymurai.transforms.entity_subcategories.regex.RegexSubcategorizer` + - `aymurai.transforms.datetime_formatter.core.DatetimeFormatter` + - `aymurai.transforms.entity_subcategories.sentence_transformer.SentenceTransformerSubcategorizer` (4 configured instances in production for `CONDUCTA`, `CONDUCTA_DESCRIPCION`, `DETALLE`, and `OBJETO_DE_LA_RESOLUCION`) + - `aymurai.transforms.entity_subcategories.article.ArticleSubcategorizer` + +### Algorithms and processing notes +- NER extraction over judicial paragraphs. +- Decision filtering/classification for relevance, gated in production so `DECISION` is emitted only when a `DETALLE` entity is already present. +- Rule-based + embedding-based subcategorization. +- Date/time normalization and article-based subcategory mapping. + +### API contracts used by this flow +- `TextRequest` +- `DocumentInformation` +- `DataPublicDocumentAnnotations` (free-form document-level validation payload) + +### Core backend modules +- Router: `aymurai/api/endpoints/routers/datapublic/datapublic.py` +- Pipeline loading and inference: datapublic predict route in `aymurai/api/endpoints/routers/datapublic/datapublic.py` + +## Persistence (DB) +Tables touched by this flow: +- `datapublic_paragraph` +- `datapublic_document` +- `datapublic_document_paragraph` + +## Notes +- Current production pipeline directory name is `datapublic`. +- `document_id` is the document-level grouping key used to associate paragraph predictions and validation payloads. +- Validation persistence is document-level and intentionally accepts a free-form JSON object. +- `GET /api/datapublic/validation/document/{document_id}` returns `404` when the document does not exist; `POST` upserts the validation payload. +- Public router currently does not mount `/api/datapublic/dataset/*` routes. +- Paragraph-level validation route exists in code as commented legacy logic and is not part of the public flow. + +## Models used by this flow +- Flair NER: [../../models/flair-model-card.md](../../models/flair-model-card.md) +- Decision classifier: [../../models/decision-model-card.md](../../models/decision-model-card.md) + +## Related docs +- Pipelines index: [../README.md](../README.md) +- Datapublic entities: [../../entities/datapublic/README.md](../../entities/datapublic/README.md) +- API reference: [../../api/README.md](../../api/README.md) +- Internal database: [../../database/README.md](../../database/README.md) diff --git a/docs/pipeline/assets/pipeline.png b/docs/pipelines/datapublic/pipeline.png similarity index 100% rename from docs/pipeline/assets/pipeline.png rename to docs/pipelines/datapublic/pipeline.png diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 00000000..ff549f0e --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,13 @@ +node_modules +build +tmp +.nvmrc +.gitignore +.editorconfig +.tmp +.next +.DS_Store +*.log +README.md +CONTRIBUTING.md +out diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 00000000..e31ba0eb --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +node_modules/* +/.pnp +.pnp.js + +# testing +/coverage +.husky + +# production +/build +/dist +/out + +# misc +.DS_Store +.env +.env.development +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Panda CSS generated files +src/renderer/src/styled + +/me/ \ No newline at end of file diff --git a/frontend/.gitlab-ci.yml b/frontend/.gitlab-ci.yml new file mode 100644 index 00000000..942f2495 --- /dev/null +++ b/frontend/.gitlab-ci.yml @@ -0,0 +1,18 @@ +--- +image: alpine:latest + +stages: + - analysis + +# This creates a temporary cache folder that prevents from +# the node_modules, and .npm (also apply for .yarn) +# to be recreated each CI run +cache: + key: ${CI_COMMIT_REF_SLUG} + paths: + - node_modules + - .npm + +include: + - local: /.gitlab/analysis/lint.yml + - local: /.gitlab/analysis/audit.yml diff --git a/frontend/.gitlab/analysis/audit.yml b/frontend/.gitlab/analysis/audit.yml new file mode 100644 index 00000000..a66d3a44 --- /dev/null +++ b/frontend/.gitlab/analysis/audit.yml @@ -0,0 +1,13 @@ +--- +audit: + stage: analysis + image: node:14 + cache: + paths: + - node_modules + - .npm + before_script: + - npm ci + script: + - npm audit + allow_failure: true diff --git a/frontend/.gitlab/analysis/lint.yml b/frontend/.gitlab/analysis/lint.yml new file mode 100644 index 00000000..bcb728c6 --- /dev/null +++ b/frontend/.gitlab/analysis/lint.yml @@ -0,0 +1,12 @@ +--- +lint: + stage: analysis + image: node:14 + cache: + paths: + - node_modules + - .npm + before_script: + - npm ci + script: + - npm run lint diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 00000000..dd854dc4 --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1 @@ +public-hoist-pattern[]=@types/* diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 00000000..544138be --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,3 @@ +{ + "singleQuote": true +} diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json new file mode 100644 index 00000000..14cfdc59 --- /dev/null +++ b/frontend/.vscode/settings.json @@ -0,0 +1,27 @@ +{ + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "biomejs.biome", + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "editor.rulers": [80, 120], + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + } +} diff --git a/frontend/CONTRIBUTING.md b/frontend/CONTRIBUTING.md new file mode 100644 index 00000000..7f08ddba --- /dev/null +++ b/frontend/CONTRIBUTING.md @@ -0,0 +1,86 @@ +# Best Practices + +## Linter + +This project uses _Create React App_ ESLint configuration as a base. You won't +be able to commit files with linting errors, and if you do a `--force` +you'll break CI. + +We also use [Markdownlint](https://github.com/DavidAnson/markdownlint) for +Markdown files. + +If you are using **VS Code**, you should install ESLint and Markdownlint +extensions. Not only it will show you linting errors but also you can set it to +fix some of them on save. To do so, edit your `settings.json` config file and +add the following: + +```json + "eslint.autoFixOnSave": true, + "eslint.alwaysShowStatus": true, + "eslint.validate": [ + "javascript", + "javascriptreact" + ] +``` + +## Branches + +- `main` is our main branch for nightly builds +- Try to not develop directly on `main` branch +- merges to `main` should be through `pull request` practice. + +### Branch naming + +- `poc/branch-name` for proof of concepts +- `feat/branch-name` for new features like, components, services, +validations, flows, etc. +- `test/branch-name` for unit/integration tests +- `fix/branch-name` for bugfixing code +- `hotfix/branch-name` for bugfixing in prod +- `docs/branch-name` for documentation + +### Pull Requests + +- Define a reviewer from the team. +- Close the branch after PR merge (except when it has the `WIP` prefix). +Otherwise this'll keep a ton unnecessary of UAT environments. +- Focus on one task at time +- Make atomic/little commits +- Push before leaving the office +- Be sure all of your tests passed + +## Commits + +We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) +specification. This will allow us to create automatized changelogs and semantic +versioning. Because of this, _you won't be able to commit messages that do not +follow this specification_. + +```text +feat: added hat wobble +^--^ ^---------------^ +| | +| +-> Summary in past simple tense. +| ++-------> Type: chore, docs, feat, fix, refactor, style, or test. +``` + +### Commit Types + +- `feat`: (new feature for the user, not a new feature for build script) +- `fix`: (bug fix for the user, not a fix to a build script) +- `docs`: (changes to the documentation) +- `style`: (formatting, missing semicolons, etc; no production code change) +- `refactor`: (refactoring production code, eg. renaming a variable) +- `test`: (adding missing tests, refactoring tests; no production code change) +- `chore`: (updating grunt tasks etc; no production code change) + +## Testing + +We use **[Jest](https://jestjs.io/)** for testing and. Tests are automatically +run during the Validate stage of the CI Pipeline. + +The pipeline enforces a 40% minimum coverage as an incentive to write tests. +It's strongly recommended that you raised this threshold over time. + +Jest can be configured via [jest.config.js](jest.config.js). diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..559f844f --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,95 @@ +# AymurAI Desktop App + +## Technologies + +### Main technologies + +- ⚛️ **React** + 🆎 **TypeScript** as the _main framework_ with _type checking_ +tools +- ⚡ **Electron** as the _deployment and packaging tool_. It also serves the +purpose of communicating the webapp with the _NodeJS_ process. +- 🪡 **Stitches** as the _styling library_ +- 🛣️ **Tanstack React Router** as the _routing library_ to navigate across the webapp +- 📄 **Mammoth** + **ExcelJS** as the _libraries_ to read and write `.docx` and +datasheet files + +### Other technologies + +- **pnpm** as _package manager_ +- **biome** as code _linter and formatter_ + +##  Getting started + +1. Clone the repository + +1. Navigate to the repository folder and install dependencies with + + ```bash + pnpm install + ``` + +1. Start the app in development mode with + + ```bash + pnpm run dev + ``` + +## Run Aymurai API + +1. First download the image + + ```sh + docker pull registry.gitlab.com/collective.ai/datagenero-public/aymurai-api-prod + ``` + +2. And then create the corresponding container + + ```sh + docker run -p 8899:8899 -h -d registry.gitlab.com/collective.ai/datagenero-public/aymurai-api-prod:latest + ``` + +## Scripts + +### Development + +- `dev:web`: starts the frontend locally to be viewed in a conventional browser +- `dev`: starts the frontend locally and creates an _Electron_ instance to view it +- `start`: previews the production build with Electron +- `start:web`: previews the production web build in a browser + +### Build + +- `build:web`: builds the React Vite project +- `build`: builds both React and Electron projects (without packaging the application) + +### Validation + +- `lint`: runs the linter (_Biome_) across the entire project +- `lint:fix`: fixes linter errors automatically +- `format`: checks code formatting with _Biome_ +- `format:fix`: fixes code formatting automatically +- `typecheck`: runs type checking on both _Node_ and _Web_ applications +- `typecheck:node`: runs type checking on the _Electron_ application +- `typecheck:web`: runs type checking on the _React_ application +- `validate`: runs linting and type checking on both React and renderer +- `pre-commit`: runs _LintStaged_ + +### Deployment + +- `build:unpack`: builds and creates an unpacked distribution directory +- `build:[win|mac|linux]`: builds and packages the application for the desired target +- `package`: cleans build directory, builds, and packages the app with Electron Forge +- `make`: cleans build directory, builds, and creates distributables with Electron Forge +- `postinstall`: installs app dependencies for Electron Builder + +##  Colaborators + +- Melina Gatto (_Project Manager_) +- Luciana Vega (_Project Manager_) +- Andy Orlandi (_Product Designer_) +- Luciano Lapenna (_Developer_) +- Ender Puentes (_Developer_) + +## Special thanks + +- Lucia Wainfeld diff --git a/frontend/a.tml b/frontend/a.tml new file mode 100644 index 00000000..ffba6f07 --- /dev/null +++ b/frontend/a.tml @@ -0,0 +1,18 @@ +// import path from "node:path"; +// import react from "@vitejs/plugin-react"; +// import { defineConfig } from "vite"; + +// export default defineConfig({ +// plugins: [react()], +// build: { +// outDir: "out", // CRA's default build output +// }, +// resolve: { +// alias: [ +// { +// find: "@", +// replacement: path.resolve(__dirname, "src"), +// }, +// ], +// }, +// }); diff --git a/frontend/biome.json b/frontend/biome.json new file mode 100644 index 00000000..676bcd39 --- /dev/null +++ b/frontend/biome.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": ["node_modules", "build", ".husky", "out", "routeTree.gen.ts"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "performance": { + "noAccumulatingSpread": "warn", + "noDelete": "warn" + }, + "correctness": { + "useExhaustiveDependencies": "warn" + }, + "complexity": { + "noUselessSwitchCase": "off", + "noForEach": "off", + "noUselessFragments": "off", + "noBannedTypes": "warn" + }, + "style": { + "noNonNullAssertion": "warn", + "useSelfClosingElements": { + "fix": "safe", + "level": "error" + } + }, + "a11y": { + "noSvgWithoutTitle": "off", + "useKeyWithClickEvents": "off" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} diff --git a/frontend/dev-app-update.yml b/frontend/dev-app-update.yml new file mode 100644 index 00000000..199fb557 --- /dev/null +++ b/frontend/dev-app-update.yml @@ -0,0 +1,3 @@ +provider: generic +url: https://example.com/auto-updates +updaterCacheDirName: vite-electron-test-updater diff --git a/frontend/docs/ANONIMIZADO.docx b/frontend/docs/ANONIMIZADO.docx new file mode 100644 index 00000000..a96d78e1 Binary files /dev/null and b/frontend/docs/ANONIMIZADO.docx differ diff --git a/frontend/docs/SIN_ANONIMIZAR.docx b/frontend/docs/SIN_ANONIMIZAR.docx new file mode 100644 index 00000000..c205bc9b Binary files /dev/null and b/frontend/docs/SIN_ANONIMIZAR.docx differ diff --git a/frontend/docs/ai.md b/frontend/docs/ai.md new file mode 100644 index 00000000..cd3f6d84 --- /dev/null +++ b/frontend/docs/ai.md @@ -0,0 +1,14 @@ +# AymurAI + +La AI está documentada en su swagger accediendo directamente a `{{AYMURAI_URL}}/api/docs` una vez que el servidor está corriendo. + +> 💡 Si la AI está corriendo en la red LAN o bajo un ngrok, se debe cambiar la ruta. + +## Uso + +Para usar la AI en la aplicación se ponen a disposición cuatro funciones en la carpeta `/src/services/aymurai`: + +- `document-extract.ts`: obtiene cada párrafo del documento en texto plano para poder renderizarlo en la aplicación. +- `predict.ts`: genera predicciones sobre los párrafos previamente extraídos. +- `anonymize.ts`: permite generar un archivo `.odt` con las anotaciones hechas por el usuario y la AI. +- `health-check.ts`: se utiliza para verificar si la api está funcionando de manera correcta. Se utiliza en la pantalla de inicio. diff --git a/frontend/docs/authentication.md b/frontend/docs/authentication.md new file mode 100644 index 00000000..b1932b0c --- /dev/null +++ b/frontend/docs/authentication.md @@ -0,0 +1,74 @@ +# Authentication + +La autenticación en la app _AymurAI_ se utiliza únicamente para el guardado de la información de la spreadsheet en el set de datos. Hay dos opciones: + +- [_Google OAuth2_](#google-oauth2) + (cargando la información en _Google Drive_) +- [_offline local_](#local) + (escribiendo en el _filesystem_). + +Actualmente la autenticación con Google se puede realizar en el paso de guardar documento. Anteriormente, esta opción se seleccionaba en la pantalla de inicio, pero fue removida ya que generaba confusión a los usuarios por dar a entender que la aplicación operaba online cuando en realidad la única instancia en la cual lo puede hacer es a la hora de guardar la spreadsheet de Drive. + +Para facilitar el proceso se creó el hook [`useLogin`](../src/hooks/useLogin.ts). + +```ts +const { logout, login } = useLogin({ + onLogout: () => ..., +}); +``` + +## Google OAuth2 + +Utiliza el _social login_ de _Google_ para poder interactuar con la +[_Spreadsheet Google API_](https://developers.google.com/sheets/api/reference/rest). +Esto permite leer/escribir en una hoja de cálculo de _Google Drive_. + +El funcionamiento es el siguiente: + +1. Una vez se presiona el botón Google la app de React se comunica con el + proceso de Node para abrir una nueva ventana que cargaría la _consent screen_. + El proceso sería: + + 1. Se genera un _challenge code_. + 2. Junto al código mencionado se añaden otros datos como el + `client_id`, `redirect_uri`, etc. + 3. Se genera una URL con los datos anteriores para acceder a + `'https://accounts.google.com/o/oauth2/...`. + 4. Se abre una nueva ventana con la URL previamente generada. + +2. Una vez abierta la _consent screen_ al comunicarse con el proceso de + Electron, se añade un _event listener_ para poder tener los datos del usuario y + guardarlos en la sesión. + +3. Esta comunicación genera un código de autorización que se utiliza para + obtener el _access token_ y el _refresh token_. Para actualizar el _access token_ + se utiliza el _refresh token_ y un timer cada 45min. + +> 💡 Para pasar la información devuelta a la app de _React_ se utiliza un +> [deep link](https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app). +> Suele suceder que en modo _development_ no quede definido el _protocol_ para el +> _deep link_, por lo que se debe usar la versión productiva. + +![Electron PKCE para OAuth2](./images/electron_pkce.png) + +> 💡 [Más información](./excel/spreadsheet-api.md) en cómo se utiliza la +> _Google Spreadsheet API_ para generar los archivos. + +## Local + +La app simplemente funciona de manera _local_ y _offline_, leyendo/escribiendo +sobre un archivo de datasheet en el filesystem. No requiere ninguna acción extra +por parte del usuario. + +Esta opción simplemente inicializa el state de la app con la siguiente +información: + +```ts +const loginOffline = (funcType: FunctionType) => { + setUser({ online: false, function: funcType, token: "" }); + // ... +}; +``` + +> 💡 [Más información](./excel/filesystem.md) en cómo y dónde se genera el +> archivo `.xlsx`. diff --git a/frontend/docs/aymurai.md b/frontend/docs/aymurai.md new file mode 100644 index 00000000..183ed62c --- /dev/null +++ b/frontend/docs/aymurai.md @@ -0,0 +1,70 @@ +# AymurAI + +## Tecnologías principales + +AymurAI es una aplicación de escritorio basada en +[NodeJS](https://nodejs.org/en/) que utiliza tres principales tecnologías: + +- **[Electron.JS](https://www.electronjs.org/)**: elegido por su capacidad de + poder crear aplicaciones multiplataforma, Electron nos permite crear + aplicaciones web que pueden ser distribuidas sin la necesidad de tener un + browser preinstalado +- **[React](https://reactjs.org/)**: elegido por su versatilidad, ReactJS es un + web framework que nos permite crear todo tipo de web apps +- **[TypeScript](https://www.typescriptlang.org/)**: un _superset_ (es + decir, que añade features al original, pero sin modificar sus principios) de + _JavaScript_ que permite crear aplicaciones con la seguridad de que el flujo de + datos respeta un orden particular y definido + +Esta combinación de frameworks y lenguajes resulta muy eficiente ya que se puede +crear una web app desde cero, distribuible facilmente y multiplataforma, y con +herramientas incluidas para poder realizar un desarrollo confiable y +con la menor cantidad de _bugs_ posibles. + +Además, el uso de _Electron_ nos permite combinar las ventajas de usar _NodeJS_ +y todos sus paquetes como proceso principal y luego usar un browser levantado +por el mismo _Electron_ basado en +[Chromium](https://www.chromium.org/chromium-projects/) para utilizar las +[Web APIs](https://developer.mozilla.org/en-US/docs/Web/API) que podemos +encontrar en cualquier browser. + +## Otras tecnologías + +Además de las tecnologías principales anteriormente mencionadas se usaron: + +- **[Stitches](https://stitches.dev/)**: elegida como librería de estilos, nos + permite configurar toda una lista de _tokens_ que pueden ser utilizados a lo + largo de toda la aplicación manteniendo una alta consistencia entre los estilos. + Esto nos permite configurar y estandarizar colores, espaciados, sombras, etc además + de darnos la posibilidad de trabajar con _styled components_ para un + ágil desarrollo +- **[Google OAuth2](https://developers.google.com/identity/protocols/oauth2)**: + sistema de autenticación de usuarios basado en [OAuth](https://oauth.net/) que + nos permite tener un acceso total a las APIs de _Google_, permitiéndonos editar + en tiempo real documentos _Spreadsheet_. Sin este sistema de autenticación no se + podría obtener el token que nos permite interaccionar con las APIs + +## Estructura + +Se dividió la aplicación en 6 páginas, que se presentan en el siguiente orden: + +1. **Login**: página en la que se selecciona cómo conectarse a la API de AyMurai y cuál funcionalidad se desea utilizar (anonimizador o set de datos) +1. **Onboarding**: se nos muestra una guía sobre el funcionamiento de la + aplicación y los pasos a seguir para poder realizar la validación de nuestros + archivos. Es en este paso donde podremos cargar nuestros documentos +1. **Preview**: obtendremos una vista previa de los escritos cargados en la + página anterior con la oportunidad de cargar nuevos documentos si es necesario. + Es aquí donde podemos decidir cuáles son los archivos finales que serán + procesados por la inteligencia artificial +1. **Process**: página de procesamiento. En este paso se realiza la consulta + a la inteligencia artificial para obtener las predicciones correspondientes + a cada archivo. Esto puede tardar un poco. Podemos detener el procesamiento + de los documentos o reemplazarlos en cualquier momento si es necesario +1. **Validation**: es en esta instancia donde validaremos lo que nos ha devuelto + la inteligencia artificial. Tendremos un form donde podremos ir seleccionando + las sugerencias hechas por la IA o completar con lo que corresponda en ese momento. + Cada vez que se valida un documento, se crea una conexión con la + _Spreadsheet API_ de _Google_ para poder enviar los datos finales +1. **Finish**: podremos ver una vista preview de los documentos que validamos y + enviamos, con la posibilidad de revisar el set de datos o de realizar una nueva + carga, reiniciando el flujo de trabajo diff --git a/frontend/docs/excel/filesystem.md b/frontend/docs/excel/filesystem.md new file mode 100644 index 00000000..28ae5b55 --- /dev/null +++ b/frontend/docs/excel/filesystem.md @@ -0,0 +1,69 @@ +# Filesystem + +Las funcionalidades de _NodeJS_ sobre el _filesystem_ se aprovechan para poder +leer y escribir sobre el set de datos de forma local y offline en la +computadora. Además, también se generan reportes que ayudan a reentrenar el +modelo de inteligencia artificial. + +## Ubicación + +Las ubicaciones para los archivos de exportación se encuentran en la carpeta: + +```ts +// Ejemplo windows: C:\Users\[Usuario]\Documents\AymurAI +const EXPORTS_FOLDER = path.resolve(homedir(), 'Documents/AymurAI'); +``` + +## API + +Para poder formar un canal de comunicación entre la aplicación de _React_ +y _Electron_, se utilizan los [_preload scripts_](https://www.electronjs.org/docs/latest/tutorial/tutorial-preload) +para exponer una _API_ que permita la lectura y escritura de archivos. + +De esta forma el _frontend_ podría acceder a las funcionalidades de _NodeJS_ +utilizando `window.filesystem.excel.read` y `window.filesystem.excel.write` por ejemplo. +Estas funciones fueron definidas dentro de los archivos de _Electron_. + +Para el caso de los reportes se utiliza `window.filesystem.feedback.export`. + +> 📚 Se pueden encontrar estos scripts y más yendo al +[script preloader del proyecto.](../../renderer/preload.ts) + +## Tipo de archivo + +### Set de datos + +El set de datos se exporta en formato _Excel_ con extensión `.xlsx`. + +```ts +const FILENAME = 'set_de_datos.xlsx'; +// Ejemplo windows: C:/Users/[Usuario]/Documents/AymurAI/set_de_datos.xlsx +const PATH = `${EXPORTS_FOLDER}/${FILENAME}`; +``` + +Desde el frontend se utiliza la librería [_ExcelJS_](https://www.npmjs.com/package/exceljs) +que permite leer un archivo `.xlsx`, modificarlo en memoria y luego escribir los +cambios en el filesystem, apoyándose en las funcionalidades de _NodeJS_. + +```ts +import { Workbook } from 'exceljs'; + +// Ejemplo de escritura de archivo +async function writeXLSX(workbook: Workbook) { + const buffer = (await workbook.xlsx.writeBuffer()) as Buffer; + + return window.filesystem.excel.write(buffer); +} +``` + +### Reportes / Feedback + +El reporte se exporta en formato _JSON_ con extensión `.json`. Utiliza el nombre +del archivo procesado y la fecha para generar el nombre del archivo. + +```ts +const FEEDBACK_FOLDER = `${EXPORTS_FOLDER}/feedback`; +const FILENAME = `${FEEDBACK_FOLDER}/{name}-{date}.json` +``` + +Se escribe en el filesystem de forma tradicional. diff --git a/frontend/docs/excel/spreadsheet-api.md b/frontend/docs/excel/spreadsheet-api.md new file mode 100644 index 00000000..a2087b40 --- /dev/null +++ b/frontend/docs/excel/spreadsheet-api.md @@ -0,0 +1,30 @@ +# Google Spreadsheet API + +Para leer y escribir de forma remota en una hoja de cálculo de _Google Sheets_ se +utiliza la [REST API](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets) +provista por _Google_. + +## Ubicación + +La ubicación del dataset está definida de forma __estática__ dentro del +[código](../../src/utils/config.ts). Esto significa que para poder acceder a +este archivo se necesita tener accesos de lectura/escritura sobre el archivo. + +## Implementación + +Dentro del proyecto se crearon una serie de funciones que ayudan a interactuar +con la API de _Google Sheets_. En general lo que hacen es _envolver_ las +llamadas a la _REST API_ utilizando el _access token_ provisto por el +[proceso de autenticación](../authentication.md). + +> Estas funciones se pueden encontrar dentro de la carpeta de +[servicios de google](../../src/services/google/). + +Por ejemplo, para obtener el contenido de una hoja de cálculo se utiliza la +función: + +```ts +import google from 'services/google'; + +await google(authToken).spreadsheet(id).range('A1:B2'); +``` diff --git a/frontend/docs/images/auth_flows.png b/frontend/docs/images/auth_flows.png new file mode 100644 index 00000000..8a98cee5 Binary files /dev/null and b/frontend/docs/images/auth_flows.png differ diff --git a/frontend/docs/images/electron_pkce.png b/frontend/docs/images/electron_pkce.png new file mode 100644 index 00000000..37c50972 Binary files /dev/null and b/frontend/docs/images/electron_pkce.png differ diff --git a/frontend/docs/images/initial-screen.png b/frontend/docs/images/initial-screen.png new file mode 100644 index 00000000..c339bdc9 Binary files /dev/null and b/frontend/docs/images/initial-screen.png differ diff --git a/frontend/docs/images/submit-form.png b/frontend/docs/images/submit-form.png new file mode 100644 index 00000000..f2505dc5 Binary files /dev/null and b/frontend/docs/images/submit-form.png differ diff --git a/frontend/docs/images/url-selector.png b/frontend/docs/images/url-selector.png new file mode 100644 index 00000000..611d7b1c Binary files /dev/null and b/frontend/docs/images/url-selector.png differ diff --git a/frontend/docs/images/validation-form.png b/frontend/docs/images/validation-form.png new file mode 100644 index 00000000..0680dd98 Binary files /dev/null and b/frontend/docs/images/validation-form.png differ diff --git a/frontend/docs/routing.md b/frontend/docs/routing.md new file mode 100644 index 00000000..d84c7e30 --- /dev/null +++ b/frontend/docs/routing.md @@ -0,0 +1,30 @@ +# Routing + +El proyecto usa `react-router-dom` para manejar las rutas de la aplicación. Las rutas se definen en el archivo `src/index.tsx` y se deben agregar en el objeto `router` de la siguiente manera: + +```jsx +const router = createRouter([ + { + // Main as a layout element + path: '/', + element: , + children: [ + { path: 'onboarding', element: }, + { path: 'preview', element: }, + { path: 'process', element: }, + ], + }, + { + path: '/login', + element: , + }, +]); +``` + +Al ser un proyecto basado en _Electron_, el router que utilizamos no es el mismo que se usa en un proyecto web normal. Para este caso, usamos un [_memory router_](https://reactrouter.com/en/main/routers/create-memory-router#creatememoryrouter) que mantiene en memoria el history stack de las rutas. + +## Protección de rutas + +Existen dos high order components que se encargan de proteger las rutas de la aplicación dependiendo qué se requiere: + +- `withAuthProtection`: verifica si el usuario está autenticado y redirige a `/login` si no lo está. diff --git a/frontend/docs/url_selector.md b/frontend/docs/url_selector.md new file mode 100644 index 00000000..2eecec44 --- /dev/null +++ b/frontend/docs/url_selector.md @@ -0,0 +1,17 @@ +# URL Selector + +En la pantalla de inicio el usuario puede elegir cómo desea conectarse a la API de AyMurAI. Hay dos opciones: + +- De manera local: esto significa, levantando Docker localmente ya sea de forma manual o utilizando el ejecutable desarrollado por el equipo de Collective AI para levantar la API utilizando **[Miniconda](https://docs.anaconda.com/miniconda/)**. + +La aplicación incluye el archivo run_server.bat que, en conjunto con el instalador desarrollado por Collective, procura levantar el server local de modo automático. El funcionamiento de esta feature está condicionada a que el usuario efectivamente tenga el directorio de Miniconda en su PC y esté utilizando Windows. En caso de que dicha ejecución falle, se muestra una alerta al usuario que le indica que deberá hacerlo de forma manual. + +- A través de un servidor: los distintos juzgados pueden tener servidores propios a las que se accede a través de su URL. + + Por razones de seguridad, no es conveniente que estas URLs estén en el código de la aplicación, por lo cual el usuario deberá ingrearlas manualmente. + + A fin de mejorar la experiencia de uso, la URL se preserva y autocompleta evitando así que el usuario tenga que reingresarla cada vez que inicia la aplicación. + +| Pantalla inicial | Selector | +| ------------------------------------------------ | ------------------------------------------ | +| ![Pantalla inicial](./images/initial-screen.png) | ![Selector URL](./images/url-selector.png) | diff --git a/frontend/docs/validator/README.md b/frontend/docs/validator/README.md new file mode 100644 index 00000000..d4d13239 --- /dev/null +++ b/frontend/docs/validator/README.md @@ -0,0 +1,55 @@ +# Validación + +El proceso de guardar la información analizada por la AI tiene que ser validado +por un usuario para asegurar que la información es correcta. La validación se +realiza medianete un formulario en el cual se carga la información que devuelve +la API y el usuario puede modificarla si es necesario. + +![Validation form view](../images/validation-form.png) + +Se desarrollaron varias herramientas para hacer más fácil la generación de las +distintas partes de la pantalla de validación. Entre ellas está el hook +`useForm` y el componente de formulario ``. + +## Hook `useForm` + +Este hook ofrece dos funciones útiles para generar formularios: + +- `submit`: una función que se encarga de recolectar toda la información de los +campos del formulario para poder utilizarla a conveniencia del desarrollador. + + ```tsx + // Ejemplo de uso + const { submit } = useForm(); + const handler = submit(data => ...) + + return
+ ``` + +- `register`: función que permite agregar campos al "state" (más bien una +lista de referencias a los ``) para luego ser recolectados por el +`submit`. Acepta un `string` que representa el "id" del campo. + + ```tsx + // Ejemplo de uso + const { register } = useForm(); + + return + ``` + +> 💡 Más información sobre cómo están compuestas y se utilizan estas funciones en +el archivo del [hook useForm](../../src/hooks/useForm/index.ts) + +## Componente `` + +La pantalla de validación se constituye por varios `
` encapsulados por el +componente `` para "estandarizar" su comportamiento. +Simplemente se encarga de envolver los campos (a los cuales se les pasa un +handler `onChange` para detectar cuando se ingresa algún valor) y de agregar un +botón de submit. + +> ⚠️ El botón de submit NO envía toda la pantalla, si no que valida el grupo de +campos individual. Para enviar TODA la información (o pasar a la siguiente +resolución) se debe usar el botón del footer. + +![Submit button](../images/submit-form.png) diff --git a/frontend/electron-builder.yml b/frontend/electron-builder.yml new file mode 100644 index 00000000..46eea126 --- /dev/null +++ b/frontend/electron-builder.yml @@ -0,0 +1,44 @@ +appId: aymurai.app +productName: AymurAI +directories: + buildResources: build +files: + - '!**/.vscode/*' + - '!src/*' + - '!electron.vite.config.{js,ts,mjs,cjs}' + - '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' + - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' + - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' +asarUnpack: + - resources/** +win: + executableName: AymurAI + icon: src/renderer/public/brand/logo.jpg +nsis: + artifactName: ${name}-${version}-setup.${ext} + shortcutName: ${productName} + uninstallDisplayName: ${productName} + createDesktopShortcut: always +mac: + entitlementsInherit: build/entitlements.mac.plist + icon: src/renderer/public/brand/logo.jpg + extendInfo: + - NSCameraUsageDescription: Application requests access to the device's camera. + - NSMicrophoneUsageDescription: Application requests access to the device's microphone. + - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. + - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. +dmg: + artifactName: ${name}-${version}.${ext} +linux: + target: + - AppImage + - snap + - deb + maintainer: datagenero.org + category: Utility +appImage: + artifactName: ${name}-${version}.${ext} +npmRebuild: false +publish: + provider: generic + url: https://example.com/auto-updates diff --git a/frontend/electron.vite.config.ts b/frontend/electron.vite.config.ts new file mode 100644 index 00000000..a0453cb7 --- /dev/null +++ b/frontend/electron.vite.config.ts @@ -0,0 +1,42 @@ +/** + * Electron-vite config for building the desktop app. + * Used by the `dev`, `build`, and `build:*` scripts. + * + * Configures all three Electron processes: main, preload, and renderer. + * `envDir` is set explicitly so `.env` files are resolved from the project root. + */ +import { tanstackRouter } from "@tanstack/router-plugin/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig, externalizeDepsPlugin } from "electron-vite"; +import { resolve } from "node:path"; + +export default defineConfig({ + main: { + plugins: [externalizeDepsPlugin()], + }, + preload: { + plugins: [externalizeDepsPlugin()], + }, + renderer: { + envDir: resolve("."), + // Use relative asset URLs so the renderer works under the file:// protocol + // when the app is packaged. The standalone web build (vite.config.ts) + // keeps the default base of "/". + base: "./", + server: { + port: 3000, + }, + resolve: { + alias: { + "@": resolve("src/renderer/src"), + }, + }, + plugins: [ + tanstackRouter({ + target: "react", + autoCodeSplitting: true, + }), + react(), + ], + }, +}); diff --git a/frontend/forge.config.js b/frontend/forge.config.js new file mode 100644 index 00000000..7c0bc436 --- /dev/null +++ b/frontend/forge.config.js @@ -0,0 +1,58 @@ +const path = require("node:path"); +const fs = require("node:fs"); + +module.exports = { + packagerConfig: { + // This forces forge to only package already builded files + ignore: ["^\\/public$", "^\\/src$", "^\\/node_modules$", "^\\/[.].+"], + icon: "public/brand/logo256-text", + name: "AymurAI", + protocols: [ + { + name: "AymurAI", + schemes: ["aymurai.app"], + }, + ], + }, + rebuildConfig: {}, + makers: [ + { + name: "@electron-forge/maker-squirrel", + config: {}, + }, + { + name: "@electron-forge/maker-zip", + platforms: ["darwin"], + }, + { + name: "@electron-forge/maker-deb", + config: { + mimeType: ["x-scheme-handler/aymurai.app"], + }, + }, + { + name: "@electron-forge/maker-rpm", + config: {}, + }, + ], + hooks: { + packageAfterCopy: async ( + _config, + buildPath, + _electronVersion, + _platform, + _arch, + ) => { + // Create logs in the root directory + const twoAbove = path.join(buildPath, "../../"); + // const logMessage = `Build Path: ${twoAbove}\nDirname: ${__dirname}\n---\n`; + // fs.appendFileSync(path.join(__dirname, "logs.txt"), logMessage); + + // biome-ignore lint/style/noVar: Don't know why this is needed + var src = path.join(__dirname, "build/app/run_server.bat"); + // biome-ignore lint/style/noVar: Don't know why this is needed + var dst = `${twoAbove}/run_server.bat`; + fs.cpSync(src, dst, { recursive: true }); + }, + }, +}; diff --git a/frontend/knip.json b/frontend/knip.json new file mode 100644 index 00000000..4651583e --- /dev/null +++ b/frontend/knip.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://unpkg.com/knip@6/schema.json", + "tags": [ + "-lintignore" + ] +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..1830abf8 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,100 @@ +{ + "name": "aymurai", + "version": "1.5.0", + "private": true, + "main": "./out/main/index.js", + "scripts": { + "dev:web": "cross-env VITE_APP_MODE=web vite --config vite.config.ts", + "dev": "cross-env VITE_APP_MODE=electron electron-vite dev", + "start": "cross-env VITE_APP_MODE=electron electron-vite preview", + "start:web": "cross-env VITE_APP_MODE=web vite preview --config vite.config.ts", + "lint": "biome check", + "lint:fix": "biome check --write", + "format": "biome format", + "format:fix": "biome format --write", + "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", + "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", + "typecheck": "npm run typecheck:node; npm run typecheck:web", + "validate": "npm run lint && npm run check-types:react && npm run check-types:renderer", + "pre-commit": "lint-staged", + "package": "rm -rf build && npm run build && electron-forge package", + "make": "rm -rf build && npm run build && electron-forge make", + "postinstall": "electron-builder install-app-deps || true", + "panda:codegen": "panda codegen", + "prepare": "panda codegen", + "build:web": "cross-env VITE_APP_MODE=web vite build --config vite.config.ts", + "build": "npm run typecheck && cross-env VITE_APP_MODE=electron electron-vite build", + "build:unpack": "npm run build && electron-builder --dir", + "build:win": "npm run build && electron-builder --win", + "build:mac": "cross-env VITE_APP_MODE=electron electron-vite build && electron-builder --mac", + "build:linux": "cross-env VITE_APP_MODE=electron electron-vite build && electron-builder --linux", + "knip": "knip", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@electron-toolkit/preload": "^3.0.2", + "@electron-toolkit/utils": "^4.0.0", + "electron-squirrel-startup": "^1.0.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@electron-forge/cli": "^6.0.4", + "@electron-forge/maker-deb": "^6.0.4", + "@electron-forge/maker-rpm": "^6.0.4", + "@electron-forge/maker-squirrel": "^6.0.4", + "@electron-forge/maker-zip": "^6.0.4", + "@electron-toolkit/tsconfig": "^1.0.1", + "@pandacss/dev": "^1.8.1", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tooltip": "^1.2.8", + "@stitches/react": "^1.2.8", + "@tanstack/react-form": "1.11.0", + "@tanstack/react-query": "^5.100.5", + "@tanstack/react-router": "^1.139.12", + "@tanstack/react-router-devtools": "^1.139.12", + "@tanstack/router-plugin": "^1.139.12", + "@types/node": "20", + "@types/react": "19", + "@types/react-dom": "19", + "@types/regex-escape": "^3.4.1", + "@vitejs/plugin-react": "^4.5.0", + "axios": "^1.12.0", + "concurrently": "^7.5.0", + "cross-env": "^7.0.3", + "electron": "^22.3.25", + "electron-builder": "^26.0.12", + "electron-debug": "^3.2.0", + "electron-devtools-installer": "^3.2.1", + "electron-vite": "^3.1.0", + "exceljs": "^4.4.0", + "husky": "^8.0.0", + "i18next": "^25.8.13", + "knip": "^6.7.0", + "lint-staged": "^13.0.3", + "markdownlint-cli": "^0.32.2", + "nodemon": "^2.0.20", + "phosphor-react": "^1.4.1", + "prettier": "^2.7.1", + "react": "19", + "react-dom": "19", + "react-hot-toast": "^2.6.0", + "react-i18next": "^16.5.4", + "regex-escape": "^3.4.10", + "typescript": "^5.9.2", + "vite": "^6.3.5", + "vitest": "^4.1.6", + "wait-on": "^6.0.1", + "zod": "^4.0.14", + "zustand": "^5.0.7" + } +} \ No newline at end of file diff --git a/frontend/panda.config.ts b/frontend/panda.config.ts new file mode 100644 index 00000000..72f545b2 --- /dev/null +++ b/frontend/panda.config.ts @@ -0,0 +1,233 @@ +import { defineConfig, defineGlobalStyles } from "@pandacss/dev"; + +const globalCss = defineGlobalStyles({ + "*": { + fontFamily: + '"Archivo", -apple-system, Helvetica Neue, Helvetica, Roboto, sans-serif', // TODO: Replace token here (was $primary) + }, + + html: { + color: "#110041", + }, + + "mark.predicted-word": { + backgroundColor: "#E6E8FF", // TODO: Replace token here (was $primaryAlt) + fontFamily: '"Times New Roman", Times, serif', // TODO: Replace token here (was $file) + padding: "0px 0px 0px 2px", + borderRadius: "8px", + + "& strong": { + fontSize: "12px", + padding: "0px", + margin: "0px", + }, + + "& button.remove-tag": { + visibility: "hidden", + position: "relative", + backgroundColor: "#DC582E", // TODO: Replace token here (was $errorPrimary) + color: "#FFFFFF", // TODO: Replace token here (was $white) + padding: "3px 5px", + borderRadius: "8px", + cursor: "pointer", + fontSize: "10px", + fontWeight: 800, // TODO: Replace token here (was $heavy) + textAlign: "center", + top: "-10px", + right: "-5px", + border: "none", + }, + + "&:hover": { + cursor: "pointer", + "& button.remove-tag": { + visibility: "visible", + }, + }, + }, + + "mark.searched-word": { + backgroundColor: "#E0DDE2", // TODO: Replace token here (was $bgSecondaryAlt) + fontFamily: '"Times New Roman", Times, serif', // TODO: Replace token here (was $file) + padding: "0px 2px", + borderRadius: "8px", + + "&:hover": { + cursor: "pointer", + }, + + "& button.add-tag": { + position: "relative", + backgroundColor: "#1B834E", // TODO: Replace token here (was $successPrimary) + color: "#FFFFFF", // TODO: Replace token here (was $white) + padding: "2px 5px", + borderRadius: "8px", + cursor: "pointer", + fontSize: "12px", + fontWeight: 800, // TODO: Replace token here (was $heavy) + textAlign: "center", + top: "-10px", + right: "-5px", + border: "none", + }, + }, +}); + +const text = (size: number, weight: number, lineHeight: string) => ({ + value: { + fontSize: `${size}px`, + fontWeight: weight, + lineHeight, + }, +}); + +const color = (hex: string) => ({ value: hex }); + +export default defineConfig({ + // Use CSS reset + preflight: true, + + // Where to look for CSS declarations + include: ["./src/renderer/src/**/*.{ts,tsx}"], + + // Files to exclude + exclude: [], + + // JSX framework + jsxFramework: "react", + + // Output directory for generated styled-system + outdir: "src/renderer/src/styled", + + // Global styles (migrated from Stitches globalStyles.ts) + globalCss, + + // Other configuration + strictTokens: true, + + theme: { + extend: { + keyframes: { + fadeIn: { + from: { opacity: "0" }, + to: { opacity: "1" }, + }, + fadeOut: { + from: { opacity: "1" }, + to: { opacity: "0" }, + }, + }, + tokens: { + animations: { + fadeIn: { value: "fadeIn 0.15s ease-out" }, + fadeOut: { value: "fadeOut 0.1s ease-in" }, + }, + }, + }, + textStyles: { + title: { + md: { + strong: text(32, 700, "120%"), + default: text(32, 400, "120%"), + }, + }, + subtitle: { + md: { + strong: text(20, 600, "120%"), + default: text(20, 400, "120%"), + }, + sm: { + strong: text(14, 600, "120%"), + default: text(14, 400, "120%"), + }, + }, + paragraph: { + md: { + strong: text(18, 600, "150%"), + default: text(18, 400, "150%"), + }, + sm: { + strong: text(16, 600, "140%"), + default: text(16, 400, "140%"), + }, + xsm: { + strong: text(10, 600, "140%"), + default: text(10, 400, "140%"), + }, + }, + cta: { + md: { + strong: text(16, 600, "100%"), + default: text(16, 400, "100%"), + }, + sm: { + strong: text(16, 600, "115%"), + default: text(16, 400, "115%"), + }, + }, + label: { + md: { + strong: text(16, 600, "120%"), + default: text(16, 400, "120%"), + }, + sm: { + strong: text(12, 600, "120%"), + default: text(12, 400, "120%"), + }, + }, + }, + semanticTokens: { + colors: { + brand: { + primary: color("#3F479D"), + secondary: color("#C3CCD7"), + tertiary: color("#4A5568"), + }, + text: { + default: color("#110041"), + lighter: color("#625C68"), + "onbutton-default": color("#110041"), + "onbutton-alternative": color("#FFFFFF"), + "onbutton-disabled": color("#2D3748"), + }, + action: { + default: color("#C5CAFF"), + disabled: color("#E0DDE2"), + "alt-default": color("#3F479D"), + hover: color("#110041"), + pressed: color("#3F479D"), + focus: color("#C5CAFF"), + }, + bg: { + primary: color("#F6F5F7"), + secondary: color("#FFFFFF"), + "primary-alternative": color("#E5E8FF"), + "primary-highlight": color("#C5CAFF"), + "secondary-highlight": color("#E0DDE2"), + }, + system: { + success: color("#1B834E"), + "success-secondary": color("#E0FAED"), + error: color("#DC582E"), + "error-secondary": color("#FFECE5"), + warning: color("#F2BA2C"), + "warning-secondary": color("#FFF7DB"), + info: color("#3F479D"), + "info-secondary": color("#F6F5F7"), + }, + }, + borders: { + primary: { value: "1px solid #BCBAB8" }, + secondary: { value: "1px solid #EDF2F7" }, + "primary-alt": { value: "1px solid #110041" }, + error: { value: "1px solid {colors.system.error}" }, + }, + gradients: { + primary: { + value: + "linear-gradient(249.5deg, #C5CAFF -33.26%, #8591E8 28.49%, #3F479D 82.99%)", + }, + }, + }, + }, +}); diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 00000000..e5bbfd38 --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,11035 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@19.2.4) + '@electron-toolkit/preload': + specifier: ^3.0.2 + version: 3.0.2(electron@22.3.27) + '@electron-toolkit/utils': + specifier: ^4.0.0 + version: 4.0.0(electron@22.3.27) + electron-squirrel-startup: + specifier: ^1.0.0 + version: 1.0.1 + devDependencies: + '@biomejs/biome': + specifier: ^1.9.4 + version: 1.9.4 + '@electron-forge/cli': + specifier: ^6.0.4 + version: 6.4.2(encoding@0.1.13) + '@electron-forge/maker-deb': + specifier: ^6.0.4 + version: 6.4.2 + '@electron-forge/maker-rpm': + specifier: ^6.0.4 + version: 6.4.2 + '@electron-forge/maker-squirrel': + specifier: ^6.0.4 + version: 6.4.2 + '@electron-forge/maker-zip': + specifier: ^6.0.4 + version: 6.4.2 + '@electron-toolkit/tsconfig': + specifier: ^1.0.1 + version: 1.0.1(@types/node@20.19.1) + '@pandacss/dev': + specifier: ^1.8.1 + version: 1.8.1(hono@4.11.7)(typescript@5.9.2) + '@radix-ui/react-dialog': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popover': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-select': + specifier: ^2.2.6 + version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-switch': + specifier: ^1.2.6 + version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-tooltip': + specifier: ^1.2.8 + version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@stitches/react': + specifier: ^1.2.8 + version: 1.2.8(react@19.2.4) + '@tanstack/react-form': + specifier: 1.11.0 + version: 1.11.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/react-query': + specifier: ^5.100.5 + version: 5.100.5(react@19.2.4) + '@tanstack/react-router': + specifier: ^1.139.12 + version: 1.139.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/react-router-devtools': + specifier: ^1.139.12 + version: 1.139.12(@tanstack/react-router@1.139.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.139.12)(@types/node@20.19.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.10)(tsx@4.20.6)(yaml@2.8.3) + '@tanstack/router-plugin': + specifier: ^1.139.12 + version: 1.139.12(@tanstack/react-router@1.139.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3)) + '@types/node': + specifier: '20' + version: 20.19.1 + '@types/react': + specifier: '19' + version: 19.2.13 + '@types/react-dom': + specifier: '19' + version: 19.2.3(@types/react@19.2.13) + '@types/regex-escape': + specifier: ^3.4.1 + version: 3.4.1 + '@vitejs/plugin-react': + specifier: ^4.5.0 + version: 4.5.0(vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3)) + axios: + specifier: ^1.12.0 + version: 1.13.4 + concurrently: + specifier: ^7.5.0 + version: 7.6.0 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + electron: + specifier: ^22.3.25 + version: 22.3.27 + electron-builder: + specifier: ^26.0.12 + version: 26.0.12(electron-builder-squirrel-windows@26.0.12) + electron-debug: + specifier: ^3.2.0 + version: 3.2.0 + electron-devtools-installer: + specifier: ^3.2.1 + version: 3.2.1 + electron-vite: + specifier: ^3.1.0 + version: 3.1.0(vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3)) + exceljs: + specifier: ^4.4.0 + version: 4.4.0 + husky: + specifier: ^8.0.0 + version: 8.0.3 + i18next: + specifier: ^25.8.13 + version: 25.8.13(typescript@5.9.2) + knip: + specifier: ^6.7.0 + version: 6.7.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + lint-staged: + specifier: ^13.0.3 + version: 13.3.0 + markdownlint-cli: + specifier: ^0.32.2 + version: 0.32.2 + nodemon: + specifier: ^2.0.20 + version: 2.0.22 + phosphor-react: + specifier: ^1.4.1 + version: 1.4.1(react@19.2.4) + prettier: + specifier: ^2.7.1 + version: 2.8.8 + react: + specifier: '19' + version: 19.2.4 + react-dom: + specifier: '19' + version: 19.2.4(react@19.2.4) + react-hot-toast: + specifier: ^2.6.0 + version: 2.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-i18next: + specifier: ^16.5.4 + version: 16.5.4(i18next@25.8.13(typescript@5.9.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.2) + regex-escape: + specifier: ^3.4.10 + version: 3.4.11 + typescript: + specifier: ^5.9.2 + version: 5.9.2 + vite: + specifier: ^6.3.5 + version: 6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3) + vitest: + specifier: ^4.1.6 + version: 4.1.6(@types/node@20.19.1)(vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3)) + wait-on: + specifier: ^6.0.1 + version: 6.0.1 + zod: + specifier: ^4.0.14 + version: 4.0.14 + zustand: + specifier: ^5.0.7 + version: 5.0.7(@types/react@19.2.13)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) + +packages: + + 7zip-bin@5.2.0: + resolution: {integrity: sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.27.3': + resolution: {integrity: sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.27.4': + resolution: {integrity: sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.27.3': + resolution: {integrity: sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.5': + resolution: {integrity: sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.27.3': + resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.27.1': + resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.27.4': + resolution: {integrity: sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.27.4': + resolution: {integrity: sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-arrow-functions@7.27.1': + resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.27.1': + resolution: {integrity: sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.5': + resolution: {integrity: sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.28.5': + resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.27.1': + resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.27.4': + resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.27.3': + resolution: {integrity: sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@1.9.4': + resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@1.9.4': + resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@1.9.4': + resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@1.9.4': + resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@1.9.4': + resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@1.9.4': + resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@1.9.4': + resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@1.9.4': + resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@1.9.4': + resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@clack/core@0.5.0': + resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==} + + '@clack/prompts@0.11.0': + resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==} + + '@csstools/postcss-cascade-layers@5.0.2': + resolution: {integrity: sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/selector-specificity@5.0.0': + resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 + + '@develar/schema-utils@2.6.5': + resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} + engines: {node: '>= 8.9.0'} + + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + + '@electron-forge/cli@6.4.2': + resolution: {integrity: sha512-bM6YVTV0uUEpIL1jkpARlSm4Li26XZn+avC/lyTdpPqnd65T/oXZNkrAD+2Jb0RlgplOaM21qWm7ybtvKDGDyA==} + engines: {node: '>= 14.17.5'} + hasBin: true + + '@electron-forge/core-utils@6.4.2': + resolution: {integrity: sha512-CjB3aakmRsXAMMDYc8PxNTMf4FdI29y4PErfv7eCXlL5oo3JW0VSKZIV7R8/Po0S0got85q2kmhZgCKuxL1BNA==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/core@6.4.2': + resolution: {integrity: sha512-VtrFZ1Q7NG1ov0jJO/tUvUiYdWZ0Y31xw762is/jfpRPD6V/soOpwJJAoWoPK9TZVkTm2pkS8S5LikCMbNCLxw==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/maker-base@6.4.2': + resolution: {integrity: sha512-zW3GH+LqDK9nxQmQEFkJPR8RqiX0lVk6a4mXll3ngujN1fPevO4ivUAWmaEVeC1dH/hXbN7s9m0S6a37MigftQ==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/maker-deb@6.4.2': + resolution: {integrity: sha512-tlV8ffivgBP94vtYXgAeXgzeKCaRyLuWH9LT8PQW1QrYbAFpCMmuwk/zFaJkyMklImCWmDFTPYMEqdEJGd7Npg==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/maker-rpm@6.4.2': + resolution: {integrity: sha512-+hfbY5pYbAer0y07OtOzVgVBHoTRmemqqZ//T0mKJpyK2ThHKGTvyW8FFlr5jlQs5LoDCM2WHKE8oGtRhivsMg==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/maker-squirrel@6.4.2': + resolution: {integrity: sha512-ukK3RcFaBrQXUzR52PsHxfwDq5XKSnj6A1kkXiyHWqgj+HIU97prBScBb5JRtasPvYN+nDdQO2vlInsLaqcx9Q==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/maker-zip@6.4.2': + resolution: {integrity: sha512-k2nfhhnxcYbUS7rCKCisuqEalxtH9l73+lrtfL0aQZiE/BLbDXyNckDIDOPvX0tBEg62nVzUdJonZwOhZVvAMw==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/plugin-base@6.4.2': + resolution: {integrity: sha512-g6AAtQ7fZ94djBmwcnWasQ8xgaNVNjgaQ00GLK0NkmQ7n0PNbsnlMDuw9vdfTiL6WaLg5nxNSYc9bFJP/rtyeA==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/publisher-base@6.4.2': + resolution: {integrity: sha512-Tnf9O8MFzdT1gsb5EDDaQUoslt7gUuUywtsr+lT/fpBlBQbei2fvioTwvZ1Q1cmsKnld7XhRh6unfgdWLTZzgw==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/shared-types@6.4.2': + resolution: {integrity: sha512-DKOUMsdTXZIq8XiqY0Hi3C+dam/JKUnvfBjwcUeyZqPdgEE1qry8xZmmjorXuLrRf1Jq8rhxYGQInSK4af0QYw==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/template-base@6.4.2': + resolution: {integrity: sha512-vsQh+64Fr2Vxg6k8DAahWq4MAdB2F2qTig+LgIJENv8ksbzC1YIq05SBAS/g2674cdr7WdwyukMy2rgxe3rhnQ==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/template-vite-typescript@6.4.2': + resolution: {integrity: sha512-h3pn6onvC/nLglmJuelYU82Qzrh0l6MqvbBGoT39bbDoRLIqmlhWTWppHgDJVXAGrSoH+9BEpptipeBQWirFwg==} + engines: {node: ^14.18.0 || >=16.0.0} + + '@electron-forge/template-vite@6.4.2': + resolution: {integrity: sha512-NX7jHRblBmIqufMbqWgpI/VnpgF/qMSTq9ZPmDSXamBhid336MC6+DoWzDpXceQZEp0m/jpMLR04ynr8O4jGlg==} + engines: {node: ^14.18.0 || >=16.0.0} + + '@electron-forge/template-webpack-typescript@6.4.2': + resolution: {integrity: sha512-MPAZQ4v6piCED7NT1LTVQf61o6Eg/laNoKbhbrFBSH1i20OUwbtV2MLj6Op292ynI9+1qdHKmFgctr6qPTCAQw==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/template-webpack@6.4.2': + resolution: {integrity: sha512-9QYr/td4cmnGOj8UF25W6An/eI+JXj9T/b+KFybL3cQ87H1yrQOn2T84Bm5/JaB4SPdIu4FdKRjqwR7C7R0g2w==} + engines: {node: '>= 14.17.5'} + + '@electron-toolkit/preload@3.0.2': + resolution: {integrity: sha512-TWWPToXd8qPRfSXwzf5KVhpXMfONaUuRAZJHsKthKgZR/+LqX1dZVSSClQ8OTAEduvLGdecljCsoT2jSshfoUg==} + peerDependencies: + electron: '>=13.0.0' + + '@electron-toolkit/tsconfig@1.0.1': + resolution: {integrity: sha512-M0Mol3odspvtCuheyujLNAW7bXq7KFNYVMRtpjFa4ZfES4MuklXBC7Nli/omvc+PRKlrklgAGx3l4VakjNo8jg==} + peerDependencies: + '@types/node': '*' + + '@electron-toolkit/utils@4.0.0': + resolution: {integrity: sha512-qXSntwEzluSzKl4z5yFNBknmPGjPa3zFhE4mp9+h0cgokY5ornAeP+CJQDBhKsL1S58aOQfcwkD3NwLZCl+64g==} + peerDependencies: + electron: '>=13.0.0' + + '@electron/asar@3.2.18': + resolution: {integrity: sha512-2XyvMe3N3Nrs8cV39IKELRHTYUWFKrmqqSY1U+GMlc0jvqjIVnoxhNd2H4JolWQncbJi1DCvb5TNxZuI2fEjWg==} + engines: {node: '>=10.12.0'} + hasBin: true + + '@electron/asar@3.4.1': + resolution: {integrity: sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==} + engines: {node: '>=10.12.0'} + hasBin: true + + '@electron/fuses@1.8.0': + resolution: {integrity: sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==} + hasBin: true + + '@electron/get@2.0.3': + resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} + engines: {node: '>=12'} + + '@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2': + resolution: {gitHosted: true, integrity: sha512-MXgzlTDEEndJB3TBbvd5uFQO/8gaINo1Hfen8vef5rq/VHVPeB63uuv/uO5+8GFsAJ/rauu6XB79S6K4+aXc+w==, tarball: https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2} + version: 10.2.0-electron.1 + engines: {node: '>=12.13.0'} + hasBin: true + + '@electron/notarize@1.2.4': + resolution: {integrity: sha512-W5GQhJEosFNafewnS28d3bpQ37/s91CDWqxVchHfmv2dQSTWpOzNlUVQwYzC1ay5bChRV/A9BTL68yj0Pa+TSg==} + engines: {node: '>= 10.0.0'} + + '@electron/notarize@2.5.0': + resolution: {integrity: sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==} + engines: {node: '>= 10.0.0'} + + '@electron/osx-sign@1.3.1': + resolution: {integrity: sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==} + engines: {node: '>=12.0.0'} + hasBin: true + + '@electron/osx-sign@1.3.3': + resolution: {integrity: sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==} + engines: {node: '>=12.0.0'} + hasBin: true + + '@electron/rebuild@3.7.0': + resolution: {integrity: sha512-VW++CNSlZwMYP7MyXEbrKjpzEwhB5kDNbzGtiPEjwYysqyTCF+YbNJ210Dj3AjWsGSV4iEEwNkmJN9yGZmVvmw==} + engines: {node: '>=12.13.0'} + hasBin: true + + '@electron/rebuild@3.7.2': + resolution: {integrity: sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg==} + engines: {node: '>=12.13.0'} + hasBin: true + + '@electron/universal@1.5.1': + resolution: {integrity: sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==} + engines: {node: '>=8.6'} + + '@electron/universal@2.0.1': + resolution: {integrity: sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==} + engines: {node: '>=16.4'} + + '@electron/windows-sign@1.2.2': + resolution: {integrity: sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==} + engines: {node: '>=14.14'} + hasBin: true + + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild/aix-ppc64@0.25.5': + resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.5': + resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.5': + resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.5': + resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.5': + resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.5': + resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.5': + resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.5': + resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.5': + resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.5': + resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.5': + resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.5': + resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.5': + resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.5': + resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.5': + resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.5': + resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.5': + resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.5': + resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.5': + resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.5': + resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.5': + resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.5': + resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.5': + resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.5': + resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.5': + resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@fast-csv/format@4.3.5': + resolution: {integrity: sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==} + + '@fast-csv/parse@4.3.6': + resolution: {integrity: sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==} + + '@floating-ui/core@1.7.4': + resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} + + '@floating-ui/dom@1.7.5': + resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} + + '@floating-ui/react-dom@2.1.7': + resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@gar/promisify@1.1.3': + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + + '@hapi/hoek@9.3.0': + resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + + '@hapi/topo@5.1.0': + resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@malept/cross-spawn-promise@1.1.1': + resolution: {integrity: sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==} + engines: {node: '>= 10'} + + '@malept/cross-spawn-promise@2.0.0': + resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==} + engines: {node: '>= 12.13.0'} + + '@malept/flatpak-bundler@0.4.0': + resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} + engines: {node: '>= 10.0.0'} + + '@modelcontextprotocol/sdk@1.25.3': + resolution: {integrity: sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@npmcli/fs@2.1.2': + resolution: {integrity: sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + '@npmcli/move-file@2.0.1': + resolution: {integrity: sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This functionality has been moved to @npmcli/fs + + '@oxc-parser/binding-android-arm-eabi@0.127.0': + resolution: {integrity: sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.127.0': + resolution: {integrity: sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.127.0': + resolution: {integrity: sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.127.0': + resolution: {integrity: sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.127.0': + resolution: {integrity: sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + resolution: {integrity: sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + resolution: {integrity: sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + resolution: {integrity: sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + resolution: {integrity: sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + resolution: {integrity: sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + resolution: {integrity: sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + resolution: {integrity: sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + resolution: {integrity: sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + resolution: {integrity: sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + resolution: {integrity: sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + resolution: {integrity: sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + resolution: {integrity: sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + resolution: {integrity: sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + resolution: {integrity: sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + resolution: {integrity: sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + + '@oxc-resolver/binding-android-arm-eabi@11.19.1': + resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.19.1': + resolution: {integrity: sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.19.1': + resolution: {integrity: sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.19.1': + resolution: {integrity: sha512-cV50vE5+uAgNcFa3QY1JOeKDSkM/9ReIcc/9wn4TavhW/itkDGrXhw9jaKnkQnGbjJ198Yh5nbX/Gr2mr4Z5jQ==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.19.1': + resolution: {integrity: sha512-xZOQiYGFxtk48PBKff+Zwoym7ScPAIVp4c14lfLxizO2LTTTJe5sx9vQNGrBymrf/vatSPNMD4FgsaaRigPkqw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1': + resolution: {integrity: sha512-lXZYWAC6kaGe/ky2su94e9jN9t6M0/6c+GrSlCqL//XO1cxi5lpAhnJYdyrKfm0ZEr/c7RNyAx3P7FSBcBd5+A==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.19.1': + resolution: {integrity: sha512-veG1kKsuK5+t2IsO9q0DErYVSw2azvCVvWHnfTOS73WE0STdLLB7Q1bB9WR+yHPQM76ASkFyRbogWo1GR1+WbQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.19.1': + resolution: {integrity: sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-musl@11.19.1': + resolution: {integrity: sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': + resolution: {integrity: sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ==} + cpu: [ppc64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': + resolution: {integrity: sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': + resolution: {integrity: sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': + resolution: {integrity: sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA==} + cpu: [s390x] + os: [linux] + + '@oxc-resolver/binding-linux-x64-gnu@11.19.1': + resolution: {integrity: sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-linux-x64-musl@11.19.1': + resolution: {integrity: sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-openharmony-arm64@11.19.1': + resolution: {integrity: sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.19.1': + resolution: {integrity: sha512-w8UCKhX826cP/ZLokXDS6+milN8y4X7zidsAttEdWlVoamTNf6lhBJldaWr3ukTDiye7s4HRcuPEPOXNC432Vg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.19.1': + resolution: {integrity: sha512-nJ4AsUVZrVKwnU/QRdzPCCrO0TrabBqgJ8pJhXITdZGYOV28TIYystV1VFLbQ7DtAcaBHpocT5/ZJnF78YJPtQ==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-ia32-msvc@11.19.1': + resolution: {integrity: sha512-EW+ND5q2Tl+a3pH81l1QbfgbF3HmqgwLfDfVithRFheac8OTcnbXt/JxqD2GbDkb7xYEqy1zNaVFRr3oeG8npA==} + cpu: [ia32] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.19.1': + resolution: {integrity: sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw==} + cpu: [x64] + os: [win32] + + '@pandacss/config@1.8.1': + resolution: {integrity: sha512-SvFvfDav//c2lbk9+IPRGCLGhUd1G8z/Nxs0CwZNLXpI0xTNy0ISGMV1ZKkqBFbmH95e+/5oZXnnM4rAS1LO0g==} + + '@pandacss/core@1.8.1': + resolution: {integrity: sha512-8axy9BApvc/oX5/K2/qX23HX1cHvp3MI/fm+CDx31v9zH3MIy4X49MMDeEU9o6L2Q6p6yinPgQZi7gtOqw+mfQ==} + + '@pandacss/dev@1.8.1': + resolution: {integrity: sha512-rHE7JEDNbqLVpybgT1Ff5R1loYxX3sF/a729EVvsBUtcd/xSHZyjhg8vnB5hXajaWt9Ro+u7chAB6+pdqd1Jog==} + hasBin: true + + '@pandacss/extractor@1.8.1': + resolution: {integrity: sha512-fgztRMZq1f/M8zdCku9ads+Wslje9/stypjN/qLjV436NI2LWktDmWYGTiEyHZTR5yEfI2LtJCa1b1BvZcU07g==} + + '@pandacss/generator@1.8.1': + resolution: {integrity: sha512-+20L+KeSbd2tqalH51M8pAMcJ6HM9rgnKXOyNAN/kx78WXec4jCZN9z9CSCFbrRxH/qpcIx20oAXZBbNghHSyQ==} + + '@pandacss/is-valid-prop@1.8.1': + resolution: {integrity: sha512-gf2HTBCOboc65Jlb9swAjbffXSIv+A4vzSQ9iHyTCDLMcXTHYjPOQNliI36WkuQgR0pNXggBbQXGNaT9wKcrAw==} + + '@pandacss/logger@1.8.1': + resolution: {integrity: sha512-u++vUM+roJPPle0PQ1BGl6GkxwusfzEbKtX35L/5GtF6+CQ1ZfbD07HlMjEzmyEQ+KeQ8joR1Ubh/YGrZdEWrQ==} + + '@pandacss/mcp@1.8.1': + resolution: {integrity: sha512-z3VOz9XVvpdQ0bf6a1REGZToFFcwU/0jyK699fRD3jzMg+OJ24j/T8RZvlFoDPCWcifjXI7eUCSnUJ1TVqXfcQ==} + + '@pandacss/node@1.8.1': + resolution: {integrity: sha512-59Cb55sFL+TO2Sd/s2SxSvh0nY/ouBsJQYqRvUkgLL56FDH28euCYvj8Tt6H9rISKl6xhCpixF7svA8YjTOuXg==} + + '@pandacss/parser@1.8.1': + resolution: {integrity: sha512-dyWX+P4zouBuvqbWqySKDWnrIeq/C2jdeS/GjswQny3pGQHwm1np9yqHfNEtKlaIouXYACbjP1cXl/tEgiKXHA==} + + '@pandacss/postcss@1.8.1': + resolution: {integrity: sha512-xopXqr/Bh/KTewxgsYe2muHIsuLqrZ64RH1cW4KRY5TOI+JaL7M4qAPTqa/fVMh+LV9k0woTd423g7Q3d2oivA==} + + '@pandacss/preset-base@1.8.1': + resolution: {integrity: sha512-NJtVQz4Dn3FuFLci8GvbXVGegKvAkYh7xB5TwJA5mqwRIOqWZ6+U/WoVXu9aR9wj3/EYAhqA5qUJQXrC6dUFmA==} + + '@pandacss/preset-panda@1.8.1': + resolution: {integrity: sha512-89QSWcSdhr1ViW6o0YMAQrSCxmY+xsrr0eA6GnNT6FWkjwL1QAtwjezWwODHN26BjlNTLmW8aK39Tepj6Wh2Aw==} + + '@pandacss/reporter@1.8.1': + resolution: {integrity: sha512-aTAObZ8ALxhGIXtHMEJ0KzDx3au1qlwHe60EA4BGGEPfBg25ntM8SaSbxOMhwUcbD5VffzywRoJeqWMaPmg3zw==} + + '@pandacss/shared@1.8.1': + resolution: {integrity: sha512-8U+iqqvb1hmTRHOCkO4q29OMv//szv/wJAQG6SPEuYth7QH+fEvPMl2qsZyDTe1b+pkB1bt7tpvTF4+lR1K8EA==} + + '@pandacss/token-dictionary@1.8.1': + resolution: {integrity: sha512-6uDH/QbxE/sS/QzfaSswxa+hMdDR1U3PUGVD/uCrt3tJ4zTHgR2icZ3vH9GN9k17WFutzcVQaKTKscY3PYn9TA==} + + '@pandacss/types@1.8.1': + resolution: {integrity: sha512-5hNn6PsbmSWdHmfETGTGW+gpFmzf0xdKlzbFHIIKHZK+zicCtHIg6iMahZsf9wvYiBu0LAKwgdKR+kDMya5N8w==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@rolldown/pluginutils@1.0.0-beta.9': + resolution: {integrity: sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==} + + '@rollup/rollup-android-arm-eabi@4.41.1': + resolution: {integrity: sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm-eabi@4.53.3': + resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.41.1': + resolution: {integrity: sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-android-arm64@4.53.3': + resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.41.1': + resolution: {integrity: sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-arm64@4.53.3': + resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.41.1': + resolution: {integrity: sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.53.3': + resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.41.1': + resolution: {integrity: sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-arm64@4.53.3': + resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.41.1': + resolution: {integrity: sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.53.3': + resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.41.1': + resolution: {integrity: sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.41.1': + resolution: {integrity: sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.41.1': + resolution: {integrity: sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.41.1': + resolution: {integrity: sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.53.3': + resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.41.1': + resolution: {integrity: sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.41.1': + resolution: {integrity: sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.41.1': + resolution: {integrity: sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.41.1': + resolution: {integrity: sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.41.1': + resolution: {integrity: sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.41.1': + resolution: {integrity: sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.53.3': + resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.41.1': + resolution: {integrity: sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.53.3': + resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.53.3': + resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.41.1': + resolution: {integrity: sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.41.1': + resolution: {integrity: sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.53.3': + resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.41.1': + resolution: {integrity: sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.53.3': + resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} + cpu: [x64] + os: [win32] + + '@sideway/address@4.1.5': + resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} + + '@sideway/formula@3.0.1': + resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} + + '@sideway/pinpoint@2.0.0': + resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@stitches/react@1.2.8': + resolution: {integrity: sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA==} + peerDependencies: + react: '>= 16.3.0' + + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + + '@tanstack/form-core@1.11.0': + resolution: {integrity: sha512-BaEnQ73h4S72AXdjgQowmDBJPHbAvPnWTY2xeY74O7/qfesZKbgPA7DQi5BW1qMv15BkthloJMi9Qljbf5mi7Q==} + + '@tanstack/history@1.139.0': + resolution: {integrity: sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg==} + engines: {node: '>=12'} + + '@tanstack/query-core@5.100.5': + resolution: {integrity: sha512-t20KrhKkf0HXzqQkPbJ5erhFesup68BAbwFgYmTrS7bxMF7O5MdmL8jUkik4thsG7Hg00fblz30h6yF1d5TxGg==} + + '@tanstack/react-form@1.11.0': + resolution: {integrity: sha512-jTsTVvRaTLWwdhCAKG0mfrH+/43wudGujwDiRRT09VYxWJeJNFoXqepXW26lY0iuq11a0+A/0lmr2TfVqPanfg==} + peerDependencies: + '@tanstack/react-start': ^1.112.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + vinxi: ^0.5.0 + peerDependenciesMeta: + '@tanstack/react-start': + optional: true + vinxi: + optional: true + + '@tanstack/react-query@5.100.5': + resolution: {integrity: sha512-aNwj1mi2v2bQ9IxkyR1grLOUkv3BYWoykHy9KDyLNbjC3tsahbOHJibK+Wjtr1wRhG59/AvJhiJG5OlthaCgJA==} + peerDependencies: + react: ^18 || ^19 + + '@tanstack/react-router-devtools@1.139.12': + resolution: {integrity: sha512-deMQGaojEJGFio95o0rDT4OhgtwfgrQIBZAGnXhfyC395n94IuE43uvvv7tkfBzWHQwYK0IvZIeyKMavbvAj7Q==} + engines: {node: '>=12'} + peerDependencies: + '@tanstack/react-router': ^1.139.12 + '@tanstack/router-core': ^1.139.12 + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + peerDependenciesMeta: + '@tanstack/router-core': + optional: true + + '@tanstack/react-router@1.139.12': + resolution: {integrity: sha512-qrIxb8c6XXih6MERZKKwdnYg0OannsQLJ/s+4/wRqKqGCG+QmvAMvnmNP7bfYLgFKi+KsE27HqUkHaSpZSenwQ==} + engines: {node: '>=12'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-store@0.7.0': + resolution: {integrity: sha512-S/Rq17HaGOk+tQHV/yrePMnG1xbsKZIl/VsNWnNXt4XW+tTY8dTlvpJH2ZQ3GRALsusG5K6Q3unAGJ2pd9W/Ng==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/react-store@0.8.0': + resolution: {integrity: sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/router-core@1.139.12': + resolution: {integrity: sha512-HCDi4fpnAFeDDogT0C61yd2nJn0FrIyFDhyHG3xJji8emdn8Ni4rfyrN4Av46xKkXTPUGdbsqih45+uuNtunew==} + engines: {node: '>=12'} + + '@tanstack/router-devtools-core@1.139.12': + resolution: {integrity: sha512-VARlT9alLnROnPsZtHrSZsqYksIdBBQ24yGzEper5K1+1e0fzpcKLnMYLK9cwr//uWA2xmQayznvBnwcTmnUlg==} + engines: {node: '>=12'} + peerDependencies: + '@tanstack/router-core': ^1.139.12 + csstype: ^3.0.10 + solid-js: '>=1.9.5' + peerDependenciesMeta: + csstype: + optional: true + + '@tanstack/router-generator@1.139.12': + resolution: {integrity: sha512-HGs35aBml+2TVwoynsEc00/9Duw19GeT1fX+JzrY0TKNfMzq/nbjR+xxU8M1w3+gHqfKiITmW70XSZoWkXu9tw==} + engines: {node: '>=12'} + + '@tanstack/router-plugin@1.139.12': + resolution: {integrity: sha512-xX39CcU6GLMaahr6YGNQYRZOQsd1WefgCH99PtY0cxZr9VNAIpJMYPsQY8h/g8A4JI10rHI1tdKxZAvodWjZxw==} + engines: {node: '>=12'} + peerDependencies: + '@rsbuild/core': '>=1.0.2' + '@tanstack/react-router': ^1.139.12 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' + vite-plugin-solid: ^2.11.10 + webpack: '>=5.92.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@tanstack/react-router': + optional: true + vite: + optional: true + vite-plugin-solid: + optional: true + webpack: + optional: true + + '@tanstack/router-utils@1.139.0': + resolution: {integrity: sha512-jT7D6NimWqoFSkid4vCno8gvTyfL1+NHpgm3es0B2UNhKKRV3LngOGilm1m6v8Qvk/gy6Fh/tvB+s+hBl6GhOg==} + engines: {node: '>=12'} + + '@tanstack/store@0.7.0': + resolution: {integrity: sha512-CNIhdoUsmD2NolYuaIs8VfWM467RK6oIBAW4nPEKZhg1smZ+/CwtCdpURgp7nxSqOaV9oKkzdWD80+bC66F/Jg==} + + '@tanstack/store@0.8.0': + resolution: {integrity: sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==} + + '@tanstack/virtual-file-routes@1.139.0': + resolution: {integrity: sha512-9PImF1d1tovTUIpjFVa0W7Fwj/MHif7BaaczgJJfbv3sDt1Gh+oW9W9uCw9M3ndEJynnp5ZD/TTs0RGubH5ssg==} + engines: {node: '>=12'} + + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + + '@ts-morph/common@0.28.1': + resolution: {integrity: sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.7': + resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/fs-extra@9.0.13': + resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + + '@types/http-cache-semantics@4.0.4': + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@14.18.63': + resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} + + '@types/node@16.18.126': + resolution: {integrity: sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==} + + '@types/node@17.0.45': + resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} + + '@types/node@20.19.1': + resolution: {integrity: sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==} + + '@types/plist@3.0.5': + resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.13': + resolution: {integrity: sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==} + + '@types/regex-escape@3.4.1': + resolution: {integrity: sha512-aQihdLwAzyST0/LTidv76ZKtltWbea61FprvpMHbqUToUpdZrSYUqPb3ToojMW2a29JfoJXBxtO6K5py4o8uwA==} + + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + + '@types/verror@1.10.11': + resolution: {integrity: sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@vitejs/plugin-react@4.5.0': + resolution: {integrity: sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + + '@vitest/expect@4.1.6': + resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==} + + '@vitest/mocker@4.1.6': + resolution: {integrity: sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.6': + resolution: {integrity: sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==} + + '@vitest/runner@4.1.6': + resolution: {integrity: sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==} + + '@vitest/snapshot@4.1.6': + resolution: {integrity: sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==} + + '@vitest/spy@4.1.6': + resolution: {integrity: sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==} + + '@vitest/utils@4.1.6': + resolution: {integrity: sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==} + + '@vue/compiler-core@3.5.25': + resolution: {integrity: sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==} + + '@vue/compiler-dom@3.5.25': + resolution: {integrity: sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==} + + '@vue/compiler-sfc@3.5.25': + resolution: {integrity: sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==} + + '@vue/compiler-ssr@3.5.25': + resolution: {integrity: sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==} + + '@vue/shared@3.5.25': + resolution: {integrity: sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==} + + '@xmldom/xmldom@0.8.10': + resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} + engines: {node: '>=10.0.0'} + deprecated: this version has critical issues, please update to the latest version + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-escapes@5.0.0: + resolution: {integrity: sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==} + engines: {node: '>=12'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + app-builder-bin@5.0.0-alpha.12: + resolution: {integrity: sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==} + + app-builder-lib@26.0.12: + resolution: {integrity: sha512-+/CEPH1fVKf6HowBUs6LcAIoRcjeqgvAeoSE+cl7Y7LndyQ9ViGPYibNk7wmhMHzNgHIuIbw4nWADPO+4mjgWw==} + engines: {node: '>=14.0.0'} + peerDependencies: + dmg-builder: 26.0.12 + electron-builder-squirrel-windows: 26.0.12 + + archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + + archiver-utils@3.0.4: + resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} + engines: {node: '>= 10'} + + archiver@5.3.2: + resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} + engines: {node: '>= 10'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + async-exit-hook@2.0.1: + resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} + engines: {node: '>=0.12.0'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + author-regex@1.0.0: + resolution: {integrity: sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==} + engines: {node: '>=0.8'} + + axios@0.25.0: + resolution: {integrity: sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==} + + axios@1.13.4: + resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==} + + babel-dead-code-elimination@1.0.10: + resolution: {integrity: sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} + hasBin: true + + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + binary@0.3.0: + resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bluebird@3.4.7: + resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.25.0: + resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + browserslist@4.28.0: + resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-equal@1.0.1: + resolution: {integrity: sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==} + engines: {node: '>=0.4'} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer-indexof-polyfill@1.0.2: + resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==} + engines: {node: '>=0.10'} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffers@0.1.1: + resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==} + engines: {node: '>=0.2.0'} + + builder-util-runtime@9.3.1: + resolution: {integrity: sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==} + engines: {node: '>=12.0.0'} + + builder-util@26.0.11: + resolution: {integrity: sha512-xNjXfsldUEe153h1DraD0XvDOpqGR0L5eKFkdReB7eFW5HqysDZFfly4rckda6y9dF39N3pkPlOblcfHKGw+uA==} + + bundle-n-require@1.1.2: + resolution: {integrity: sha512-bEk2jakVK1ytnZ9R2AAiZEeK/GxPUM8jvcRxHZXifZDMcjkI4EG/GlsJ2YGSVYT9y/p/gA9/0yDY8rCGsSU6Tg==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cacache@16.1.3: + resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + caniuse-api@3.0.0: + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} + + caniuse-lite@1.0.30001720: + resolution: {integrity: sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==} + + caniuse-lite@1.0.30001766: + resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chainsaw@0.1.0: + resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chromium-pickle-js@0.2.0: + resolution: {integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-cursor@4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + + cli-truncate@3.1.0: + resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@11.0.0: + resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==} + engines: {node: '>=16'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + + commander@9.4.1: + resolution: {integrity: sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==} + engines: {node: ^12.20.0 || >=14} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + compare-version@0.1.2: + resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} + engines: {node: '>=0.10.0'} + + compress-commons@4.1.2: + resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} + engines: {node: '>= 10'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concurrently@7.6.0: + resolution: {integrity: sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==} + engines: {node: ^12.20.0 || ^14.13.0 || >=16.0.0} + hasBin: true + + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + + config-file-ts@0.2.8-rc1: + resolution: {integrity: sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==} + + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-es@2.0.0: + resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@4.0.3: + resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} + engines: {node: '>= 10'} + + crc@3.8.0: + resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} + + cross-dirname@0.1.0: + resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==} + + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + + cross-spawn-windows-exe@1.2.0: + resolution: {integrity: sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw==} + engines: {node: '>= 10'} + + cross-spawn@6.0.6: + resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} + engines: {node: '>=4.8'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cross-zip@4.0.1: + resolution: {integrity: sha512-n63i0lZ0rvQ6FXiGQ+/JFCKAUyPFhLQYJIqKaa+tSJtfKeULF/IDNDAbdnSIxgS4NTuw2b0+lj8LzfITuq+ZxQ==} + engines: {node: '>=12.10'} + + crosspath@2.0.0: + resolution: {integrity: sha512-ju88BYCQ2uvjO2bR+SsgLSTwTSctU+6Vp2ePbKPgSCZyy4MWZxYsT738DlKVRE5utUjobjPRm1MkTYKJxCmpTA==} + engines: {node: '>=14.9.0'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssnano-utils@5.0.1: + resolution: {integrity: sha512-ZIP71eQgG9JwjVZsTPSqhc6GHgEr53uJ7tK5///VfyWj6Xp2DBmixWHqJgPno+PqATzn48pL42ww9x5SSGmhZg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-formdata@0.9.0: + resolution: {integrity: sha512-q5uwOjR3Um5YD+ZWPOF/1sGHVW9A5rCrRwITQChRXlmPkxDFBqCm4jNTIVdGHNH9OnR+V9MoZVgRhsFb+ARbUw==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + + devalue@5.1.1: + resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} + + diff@8.0.2: + resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} + engines: {node: '>=0.3.1'} + + dir-compare@3.3.0: + resolution: {integrity: sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==} + + dir-compare@4.2.0: + resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} + + dmg-builder@26.0.12: + resolution: {integrity: sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==} + + dmg-license@1.0.11: + resolution: {integrity: sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==} + engines: {node: '>=8'} + os: [darwin] + hasBin: true + + dotenv-expand@11.0.7: + resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} + engines: {node: '>=12'} + + dotenv@16.5.0: + resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-builder-squirrel-windows@26.0.12: + resolution: {integrity: sha512-kpwXM7c/ayRUbYVErQbsZ0nQZX4aLHQrPEG9C4h9vuJCXylwFH8a7Jgi2VpKIObzCXO7LKHiCw4KdioFLFOgqA==} + + electron-builder@26.0.12: + resolution: {integrity: sha512-cD1kz5g2sgPTMFHjLxfMjUK5JABq3//J4jPswi93tOPFz6btzXYtK5NrDt717NRbukCUDOrrvmYVOWERlqoiXA==} + engines: {node: '>=14.0.0'} + hasBin: true + + electron-debug@3.2.0: + resolution: {integrity: sha512-7xZh+LfUvJ52M9rn6N+tPuDw6oRAjxUj9SoxAZfJ0hVCXhZCsdkrSt7TgXOiWiEOBgEV8qwUIO/ScxllsPS7ow==} + + electron-devtools-installer@3.2.1: + resolution: {integrity: sha512-FaCi+oDCOBTw0gJUsuw5dXW32b2Ekh5jO8lI1NRCQigo3azh2VogsIi0eelMVrP1+LkN/bewyH3Xoo1USjO0eQ==} + + electron-installer-common@0.10.4: + resolution: {integrity: sha512-8gMNPXfAqUE5CfXg8RL0vXpLE9HAaPkgLXVoHE3BMUzogMWenf4LmwQ27BdCUrEhkjrKl+igs2IHJibclR3z3Q==} + engines: {node: '>= 10.0.0'} + + electron-installer-debian@3.2.0: + resolution: {integrity: sha512-58ZrlJ1HQY80VucsEIG9tQ//HrTlG6sfofA3nRGr6TmkX661uJyu4cMPPh6kXW+aHdq/7+q25KyQhDrXvRL7jw==} + engines: {node: '>= 10.0.0'} + os: [darwin, linux] + hasBin: true + + electron-installer-redhat@3.4.0: + resolution: {integrity: sha512-gEISr3U32Sgtj+fjxUAlSDo3wyGGq6OBx7rF5UdpIgbnpUvMN4W5uYb0ThpnAZ42VEJh/3aODQXHbFS4f5J3Iw==} + engines: {node: '>= 10.0.0'} + os: [darwin, linux] + hasBin: true + + electron-is-accelerator@0.1.2: + resolution: {integrity: sha512-fLGSAjXZtdn1sbtZxx52+krefmtNuVwnJCV2gNiVt735/ARUboMl8jnNC9fZEqQdlAv2ZrETfmBUsoQci5evJA==} + + electron-is-dev@1.2.0: + resolution: {integrity: sha512-R1oD5gMBPS7PVU8gJwH6CtT0e6VSoD0+SzSnYpNm+dBkcijgA+K7VAMHDfnRq/lkKPZArpzplTW6jfiMYosdzw==} + + electron-localshortcut@3.2.1: + resolution: {integrity: sha512-DWvhKv36GsdXKnaFFhEiK8kZZA+24/yFLgtTwJJHc7AFgDjNRIBJZ/jq62Y/dWv9E4ypYwrVWN2bVrCYw1uv7Q==} + + electron-packager@17.1.2: + resolution: {integrity: sha512-XofXdikjYI7MVBcnXeoOvRR+yFFFHOLs3J7PF5KYQweigtgLshcH4W660PsvHr4lYZ03JBpLyEcUB8DzHZ+BNw==} + engines: {node: '>= 14.17.5'} + deprecated: Please use @electron/packager moving forward. There is no API change, just a package name change + hasBin: true + + electron-publish@26.0.11: + resolution: {integrity: sha512-a8QRH0rAPIWH9WyyS5LbNvW9Ark6qe63/LqDB7vu2JXYpi0Gma5Q60Dh4tmTqhOBQt0xsrzD8qE7C+D7j+B24A==} + + electron-squirrel-startup@1.0.1: + resolution: {integrity: sha512-sTfFIHGku+7PsHLJ7v0dRcZNkALrV+YEozINTW8X1nM//e5O3L+rfYuvSW00lmGHnYmUjARZulD8F2V8ISI9RA==} + + electron-to-chromium@1.5.161: + resolution: {integrity: sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA==} + + electron-to-chromium@1.5.283: + resolution: {integrity: sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==} + + electron-vite@3.1.0: + resolution: {integrity: sha512-M7aAzaRvSl5VO+6KN4neJCYLHLpF/iWo5ztchI/+wMxIieDZQqpbCYfaEHHHPH6eupEzfvZdLYdPdmvGqoVe0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@swc/core': ^1.0.0 + vite: ^4.0.0 || ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + '@swc/core': + optional: true + + electron-winstaller@5.4.0: + resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} + engines: {node: '>=8.0.0'} + + electron@22.3.27: + resolution: {integrity: sha512-7Rht21vHqj4ZFRnKuZdFqZFsvMBCmDqmjetiMqPtF+TmTBiGne1mnstVXOA/SRGhN2Qy5gY5bznJKpiqogjM8A==} + engines: {node: '>= 12.20.55'} + hasBin: true + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + entities@3.0.1: + resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==} + engines: {node: '>=0.12'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + + esbuild@0.25.5: + resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + exceljs@4.4.0: + resolution: {integrity: sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==} + engines: {node: '>=8.3.0'} + + execa@1.0.0: + resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} + engines: {node: '>=6'} + + execa@7.2.0: + resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==} + engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} + + expand-tilde@2.0.2: + resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} + engines: {node: '>=0.10.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + extsprintf@1.4.1: + resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} + engines: {'0': node >=0.6.0} + + fast-csv@4.3.6: + resolution: {integrity: sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==} + engines: {node: '>=10.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fd-package-json@2.0.0: + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fdir@6.4.5: + resolution: {integrity: sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + + filename-reserved-regex@2.0.0: + resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==} + engines: {node: '>=4'} + + filenamify@4.3.0: + resolution: {integrity: sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==} + engines: {node: '>=8'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-up@2.1.0: + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} + engines: {node: '>=4'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flora-colossus@2.0.0: + resolution: {integrity: sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA==} + engines: {node: '>= 12'} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + formatly@0.3.0: + resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} + engines: {node: '>=18.3.0'} + hasBin: true + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.3.0: + resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + engines: {node: '>=14.14'} + + fs-extra@11.3.2: + resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} + engines: {node: '>=14.14'} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fstream@1.0.12: + resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==} + engines: {node: '>=0.6'} + deprecated: This package is no longer supported. + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + galactus@1.0.0: + resolution: {integrity: sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ==} + engines: {node: '>= 12'} + + gar@1.0.4: + resolution: {integrity: sha512-w4n9cPWyP7aHxKxYHFQMegj7WIAsL/YX/C4Bs5Rr8s1H9M1rNtRWRsw+ovYMkXDQ5S4ZbYHsHAPmevPjPgw44w==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-folder-size@2.0.1: + resolution: {integrity: sha512-+CEb+GDCM7tkOS2wdMKTn9vU7DgnKUTuDlehkNJKNSovdCOVxs14OfKCk4cvSaR3za4gj+OBdl9opPN9xrJ0zA==} + hasBin: true + + get-installed-path@2.1.1: + resolution: {integrity: sha512-Qkn9eq6tW5/q9BDVdMpB8tOHljX9OSP0jRC5TRNVA4qRc839t4g8KQaR8t0Uv0EFVL0MlyG7m/ofjEgAROtYsA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-package-info@1.0.0: + resolution: {integrity: sha512-SCbprXGAPdIhKAXiG+Mk6yeoFH61JlYunqdFQFHDtLjJlDjFf6x07dsS8acO+xWt52jpdVo49AlVDnUVK1sDNw==} + engines: {node: '>= 4.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stdin@9.0.0: + resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} + engines: {node: '>=12'} + + get-stream@4.1.0: + resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} + engines: {node: '>=6'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + glob@8.0.3: + resolution: {integrity: sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==} + engines: {node: '>=12'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + + global-modules@1.0.0: + resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} + engines: {node: '>=0.10.0'} + + global-prefix@1.0.2: + resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} + engines: {node: '>=0.10.0'} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + goober@2.1.18: + resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==} + peerDependencies: + csstype: ^3.0.10 + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + homedir-polyfill@1.0.3: + resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} + engines: {node: '>=0.10.0'} + + hono@4.11.7: + resolution: {integrity: sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==} + engines: {node: '>=16.9.0'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@4.3.1: + resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} + engines: {node: '>=14.18.0'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + husky@8.0.3: + resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} + engines: {node: '>=14'} + hasBin: true + + i18next@25.8.13: + resolution: {integrity: sha512-E0vzjBY1yM+nsFrtgkjLhST2NBkirkvOVoQa0MSldhsuZ3jUge7ZNpuwG0Cfc74zwo5ZwRzg3uOgT+McBn32iA==} + peerDependencies: + typescript: ^5 + peerDependenciesMeta: + typescript: + optional: true + + iconv-corefoundation@1.1.7: + resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} + engines: {node: ^8.11.2 || >=10} + os: [darwin] + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + + ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@3.0.1: + resolution: {integrity: sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + interpret@3.1.1: + resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} + engines: {node: '>=10.13.0'} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-ci@3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isbinaryfile@4.0.10: + resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} + engines: {node: '>= 8.0.0'} + + isbinaryfile@5.0.4: + resolution: {integrity: sha512-YKBKVkKhty7s8rxddb40oOkuP0NbaeXrQvLin6QMHL7Ypiy2RW9LwOVrVgZRyOrhQlayMd9t+D8yDy8MKFTSDQ==} + engines: {node: '>= 18.0.0'} + + isbot@5.1.32: + resolution: {integrity: sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + engines: {node: '>=10'} + hasBin: true + + javascript-stringify@2.1.0: + resolution: {integrity: sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + joi@17.13.3: + resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.1.0: + resolution: {integrity: sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==} + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + + junk@3.1.0: + resolution: {integrity: sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==} + engines: {node: '>=8'} + + keyboardevent-from-electron-accelerator@2.0.0: + resolution: {integrity: sha512-iQcmNA0M4ETMNi0kG/q0h/43wZk7rMeKYrXP7sqKIJbHkTU8Koowgzv+ieR/vWJbOwxx5nDC3UnudZ0aLSu4VA==} + + keyboardevents-areequal@0.2.2: + resolution: {integrity: sha512-Nv+Kr33T0mEjxR500q+I6IWisOQ0lK1GGOncV0kWE6n4KFmpcu7RUX5/2B0EUtX51Cb0HjZ9VJsSY3u4cBa0kw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + knip@6.7.0: + resolution: {integrity: sha512-ckL51NDH1YJxnv1kNB0iUdDngB4f/e9Igz8uIqYfmNDoyOFmmk1V0WFv3LQ7/hzC63b2Z9X41gGUE9eOWrZpaA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + lazy-val@1.0.5: + resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + linkify-it@4.0.1: + resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==} + + lint-staged@13.3.0: + resolution: {integrity: sha512-mPRtrYnipYYv1FEE134ufbWpeggNTo+O/UPzngoaKzbzHAthvR55am+8GfHTnqNRQVRRrYQLGW9ZyUoD7DsBHQ==} + engines: {node: ^16.14.0 || >=18.0.0} + hasBin: true + + listenercount@1.0.1: + resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==} + + listr2@5.0.8: + resolution: {integrity: sha512-mC73LitKHj9w6v30nLNGPetZIlfpUniNSsxxrbaPcWOjDb92SHPzJPi/t+v1YC/lxKz/AJ9egOjww0qUuFxBpA==} + engines: {node: ^14.13.1 || >=16.0.0} + peerDependencies: + enquirer: '>= 2.3.0 < 3' + peerDependenciesMeta: + enquirer: + optional: true + + listr2@6.6.1: + resolution: {integrity: sha512-+rAXGHh0fkEWdXBmX+L6mmfmXmXvDGEKzkjxO+8mP3+nI/r/CWznVBvsibXdxda9Zz0OW2e2ikphN3OwCT/jSg==} + engines: {node: '>=16.0.0'} + peerDependencies: + enquirer: '>= 2.3.0 < 3' + peerDependenciesMeta: + enquirer: + optional: true + + load-json-file@2.0.0: + resolution: {integrity: sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ==} + engines: {node: '>=4'} + + locate-path@2.0.0: + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} + engines: {node: '>=4'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + + lodash.escaperegexp@4.1.2: + resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + + lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + + lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. + + lodash.groupby@4.6.0: + resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + + lodash.isfunction@3.0.9: + resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} + + lodash.isnil@4.0.0: + resolution: {integrity: sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isundefined@3.0.1: + resolution: {integrity: sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + log-update@4.0.0: + resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} + engines: {node: '>=10'} + + log-update@5.0.1: + resolution: {integrity: sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + look-it-up@2.1.0: + resolution: {integrity: sha512-nMoGWW2HurtuJf6XAL56FWTDCWLOTSsanrgwOyaR5Y4e3zfG5N/0cU5xWZSEU3tBxhQugRbV1xL9jb+ug7yZww==} + + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + make-fetch-happen@10.2.1: + resolution: {integrity: sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + map-age-cleaner@0.1.3: + resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} + engines: {node: '>=6'} + + markdown-it@13.0.1: + resolution: {integrity: sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==} + hasBin: true + + markdownlint-cli@0.32.2: + resolution: {integrity: sha512-xmJT1rGueUgT4yGNwk6D0oqQr90UJ7nMyakXtqjgswAkEhYYqjHew9RY8wDbOmh2R270IWjuKSeZzHDEGPAUkQ==} + engines: {node: '>=14'} + hasBin: true + + markdownlint-rule-helpers@0.17.2: + resolution: {integrity: sha512-XaeoW2NYSlWxMCZM2B3H7YTG6nlaLfkEZWMBhr4hSPlq9MuY2sy83+Xr89jXOqZMZYjvi5nBCGoFh7hHoPKZmA==} + engines: {node: '>=12'} + + markdownlint@0.26.2: + resolution: {integrity: sha512-2Am42YX2Ex5SQhRq35HxYWDfz1NLEOZWWN25nqd2h3AHRKsGRE+Qg1gt1++exW792eXTrR4jCNHfShfWk9Nz8w==} + engines: {node: '>=14'} + + matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdurl@1.0.1: + resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + mem@4.3.0: + resolution: {integrity: sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==} + engines: {node: '>=6'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + microdiff@1.5.0: + resolution: {integrity: sha512-Drq+/THMvDdzRYrK0oxJmOKiC24ayUV8ahrt8l3oRK51PWt6gdtrIGrlIH3pT/lFh1z93FbAcidtsHcWbnRz8Q==} + + micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@10.0.1: + resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} + engines: {node: 20 || >=22} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass-collect@1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} + + minipass-fetch@2.1.2: + resolution: {integrity: sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + minipass-flush@1.0.7: + resolution: {integrity: sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + nice-try@1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + + node-abi@3.75.0: + resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==} + engines: {node: '>=10'} + + node-addon-api@1.7.2: + resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} + + node-api-version@0.2.1: + resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==} + + node-eval@2.0.0: + resolution: {integrity: sha512-Ap+L9HznXAVeJj3TJ1op6M6bg5xtTq8L5CU/PJxtkhea/DrIxdTknGKIECKd/v/Lgql95iuMAYvIzBNd0pmcMg==} + engines: {node: '>= 4'} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + nodemon@2.0.22: + resolution: {integrity: sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==} + engines: {node: '>=8.10.0'} + hasBin: true + + nopt@6.0.0: + resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + hasBin: true + + normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + + npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object-path@0.11.8: + resolution: {integrity: sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA==} + engines: {node: '>= 10.12.0'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + outdent@0.8.0: + resolution: {integrity: sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==} + + oxc-parser@0.127.0: + resolution: {integrity: sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.19.1: + resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==} + + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + + p-defer@1.0.0: + resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==} + engines: {node: '>=4'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-is-promise@2.1.0: + resolution: {integrity: sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==} + engines: {node: '>=6'} + + p-limit@1.3.0: + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} + engines: {node: '>=4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + + p-locate@2.0.0: + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} + engines: {node: '>=4'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + p-try@1.0.0: + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} + engines: {node: '>=4'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + parse-author@2.0.0: + resolution: {integrity: sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==} + engines: {node: '>=0.10.0'} + + parse-json@2.2.0: + resolution: {integrity: sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==} + engines: {node: '>=0.10.0'} + + parse-passwd@1.0.0: + resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} + engines: {node: '>=0.10.0'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + path-type@2.0.0: + resolution: {integrity: sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==} + engines: {node: '>=4'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pe-library@0.4.1: + resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==} + engines: {node: '>=12', npm: '>=6'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + phosphor-react@1.4.1: + resolution: {integrity: sha512-gO5j7U0xZrdglTAYDYPACU4xDOFBTJmptrrB/GeR+tHhCZF3nUMyGmV/0hnloKjuTrOmpSFlbfOY78H39rgjUQ==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + plist@3.1.0: + resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} + engines: {node: '>=10.4.0'} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + postcss-discard-duplicates@7.0.2: + resolution: {integrity: sha512-eTonaQvPZ/3i1ASDHOKkYwAybiM45zFIc7KXils4mQmHLqIswXD9XNOKEVxtTFnsmwYzF66u4LMgSr0abDlh5w==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-discard-empty@7.0.1: + resolution: {integrity: sha512-cFrJKZvcg/uxB6Ijr4l6qmn3pXQBna9zyrPC+sK0zjbkDUZew+6xDltSF7OeB7rAtzaaMVYSdbod+sZOCWnMOg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-merge-rules@7.0.7: + resolution: {integrity: sha512-njWJrd/Ms6XViwowaaCc+/vqhPG3SmXn725AGrnl+BgTuRPEacjiLEaGq16J6XirMJbtKkTwnt67SS+e2WGoew==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-minify-selectors@7.0.5: + resolution: {integrity: sha512-x2/IvofHcdIrAm9Q+p06ZD1h6FPcQ32WtCRVodJLDR+WMn8EVHI1kvLxZuGKz/9EY5nAmI6lIQIrpo4tBy5+ug==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-nested@7.0.2: + resolution: {integrity: sha512-5osppouFc0VR9/VYzYxO03VaDa3e8F23Kfd6/9qcZTUI8P58GIYlArOET2Wq0ywSl2o2PjELhYOFI4W7l5QHKw==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-normalize-whitespace@7.0.1: + resolution: {integrity: sha512-vsbgFHMFQrJBJKrUFJNZ2pgBeBkC2IvvoHjz1to0/0Xk7sII24T0qFOiJzG6Fu3zJoq/0yI4rKWi7WhApW+EFA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.4: + resolution: {integrity: sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + postject@1.0.0-alpha.6: + resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==} + engines: {node: '>=14.0.0'} + hasBin: true + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + prettier@3.2.5: + resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} + engines: {node: '>=14'} + hasBin: true + + prettier@3.7.3: + resolution: {integrity: sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==} + engines: {node: '>=14'} + hasBin: true + + proc-log@2.0.1: + resolution: {integrity: sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + + pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rcedit@3.1.0: + resolution: {integrity: sha512-WRlRdY1qZbu1L11DklT07KuHfRk42l0NFFJdaExELEu4fEQ982bP5Z6OWGPj/wLLIuKRQDCxZJGAwoFsxhZhNA==} + engines: {node: '>= 10.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + + react-hot-toast@2.6.0: + resolution: {integrity: sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + + react-i18next@16.5.4: + resolution: {integrity: sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g==} + peerDependencies: + i18next: '>= 25.6.2' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + + read-binary-file-arch@1.0.6: + resolution: {integrity: sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==} + hasBin: true + + read-pkg-up@2.0.0: + resolution: {integrity: sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w==} + engines: {node: '>=4'} + + read-pkg@2.0.0: + resolution: {integrity: sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA==} + engines: {node: '>=4'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + recast@0.23.11: + resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} + engines: {node: '>= 4'} + + rechoir@0.8.0: + resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} + engines: {node: '>= 10.13.0'} + + regex-escape@3.4.11: + resolution: {integrity: sha512-051l4Hl/0HoJwTvNztrWVjoxLiseSfCrDgWqwR1cnGM/nyQSeIjmvti5zZ7HzOmsXDPaJ2k0iFxQ6/WNpJD5wQ==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resedit@1.7.2: + resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} + engines: {node: '>=12', npm: '>=6'} + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + resolve-dir@1.0.1: + resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} + engines: {node: '>=0.10.0'} + + resolve-package@1.0.1: + resolution: {integrity: sha512-rzB7NnQpOkPHBWFPP3prUMqOP6yg3HkRGgcvR+lDyvyHoY3fZLFLYDkPXh78SPVBAE6VTCk/V+j8we4djg6o4g==} + engines: {node: '>=4', npm: '>=2'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + restore-cursor@4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@2.6.3: + resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + + rollup@4.41.1: + resolution: {integrity: sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rollup@4.53.3: + resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + run-con@1.2.12: + resolution: {integrity: sha512-5257ILMYIF4RztL9uoZ7V9Q97zHtNHn5bN3NobeAnzB1P3ASLgg8qocM2u+R18ttp+VEM78N2LK8XcNVtnSRrg==} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sanitize-filename@1.6.3: + resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} + + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + + saxes@5.0.1: + resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} + engines: {node: '>=10'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.0.0: + resolution: {integrity: sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + + seroval-plugins@1.3.3: + resolution: {integrity: sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval-plugins@1.4.0: + resolution: {integrity: sha512-zir1aWzoiax6pbBVjoYVd0O1QQXgIL3eVGBMsBsNmM8Ukq90yGaWlfx0AB9dTS8GPqrOrbXn79vmItCUP9U3BQ==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.3.2: + resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} + engines: {node: '>=10'} + + seroval@1.4.0: + resolution: {integrity: sha512-BdrNXdzlofomLTiRnwJTSEAaGKyHHZkbMXIywOh7zlzp4uZnXErEwl9XZ+N1hJSNpeTtNxWvVwN0wUzAIQ4Hpg==} + engines: {node: '>=10'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.2: + resolution: {integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==} + engines: {node: '>= 0.4'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-update-notifier@1.1.0: + resolution: {integrity: sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==} + engines: {node: '>=8.10.0'} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + engines: {node: '>= 18'} + + socks-proxy-agent@7.0.0: + resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==} + engines: {node: '>= 10'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + solid-js@1.9.10: + resolution: {integrity: sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + spawn-command@0.0.2: + resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.21: + resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + ssri@9.0.1: + resolution: {integrity: sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + stat-mode@1.0.0: + resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==} + engines: {node: '>= 6'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + + strip-outer@1.0.1: + resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==} + engines: {node: '>=0.10.0'} + + sudo-prompt@9.2.1: + resolution: {integrity: sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + sumchecker@3.0.1: + resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} + engines: {node: '>= 8.0'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + temp-file@3.4.0: + resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} + + temp@0.9.4: + resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} + engines: {node: '>=6.0.0'} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tiny-async-pool@1.3.0: + resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==} + + tiny-each-async@2.0.3: + resolution: {integrity: sha512-5ROII7nElnAirvFn8g7H7MtpfV1daMcyfTGQwsn/x2VtyV+VPiO5CjReCJtWLvoKTDEDmZocf3cNPraiMnBXLA==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.1.2: + resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + engines: {node: '>=18'} + + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + + tmp@0.2.3: + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + traverse@0.3.9: + resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + trim-repeated@1.0.0: + resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==} + engines: {node: '>=0.10.0'} + + truncate-utf8-bytes@1.0.2: + resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} + + ts-evaluator@1.2.0: + resolution: {integrity: sha512-ncSGek1p92bj2ifB7s9UBgryHCkU9vwC5d+Lplt12gT9DH+e41X8dMoHRQjIMeAvyG7j9dEnuHmwgOtuRIQL+Q==} + engines: {node: '>=14.19.0'} + peerDependencies: + jsdom: '>=14.x || >=15.x || >=16.x || >=17.x || >=18.x || >=19.x || >=20.x || >=21.x || >=22.x' + typescript: '>=3.2.x || >= 4.x || >= 5.x' + peerDependenciesMeta: + jsdom: + optional: true + + ts-morph@27.0.2: + resolution: {integrity: sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==} + + ts-pattern@5.9.0: + resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==} + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.20.6: + resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@1.0.6: + resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} + + unbash@3.0.0: + resolution: {integrity: sha512-FeFPZ/WFT0mbRCuydiZzpPFlrYN8ZUpphQKoq4EeElVIYjYyGzPMxQR/simUwCOJIyVhpFk4RbtyO7RuMpMnHA==} + engines: {node: '>=14'} + + undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unique-filename@2.0.1: + resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + unique-slug@3.0.0: + resolution: {integrity: sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + unzip-crx-3@0.2.0: + resolution: {integrity: sha512-0+JiUq/z7faJ6oifVB5nSwt589v1KCduqIJupNVDoWSXZtWDmjDGO3RAEOvwJ07w90aoXoP4enKsR7ecMrJtWQ==} + + unzipper@0.10.14: + resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + username@5.1.0: + resolution: {integrity: sha512-PCKbdWw85JsYMvmCv5GH3kXmM66rCd9m1hBEDutPNv94b/pqCMT4NtcKyeWYvLFiE8b+ha1Jdl8XAaUdPn5QTg==} + engines: {node: '>=8'} + + utf8-byte-length@1.0.5: + resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + verror@1.10.1: + resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} + engines: {node: '>=0.6.0'} + + vite@6.3.5: + resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vite@7.2.4: + resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.6: + resolution: {integrity: sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.6 + '@vitest/browser-preview': 4.1.6 + '@vitest/browser-webdriverio': 4.1.6 + '@vitest/coverage-istanbul': 4.1.6 + '@vitest/coverage-v8': 4.1.6 + '@vitest/ui': 4.1.6 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + + wait-on@6.0.1: + resolution: {integrity: sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw==} + engines: {node: '>=10.0.0'} + hasBin: true + + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrapjs@5.1.1: + resolution: {integrity: sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==} + engines: {node: '>=12.17'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaku@0.16.7: + resolution: {integrity: sha512-Syu3IB3rZvKvYk7yTiyl1bo/jiEFaaStrgv1V2TIJTqYPStSMQVO8EQjg/z+DRzLq/4LIIharNT3iH1hylEIRw==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yaml@2.3.1: + resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} + engines: {node: '>= 14'} + + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yarn-or-npm@3.0.1: + resolution: {integrity: sha512-fTiQP6WbDAh5QZAVdbMQkecZoahnbOjClTQhzv74WX5h2Uaidj1isf9FDes11TKtsZ0/ZVfZsqZ+O3x6aLERHQ==} + engines: {node: '>=8.6.0'} + hasBin: true + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + + zip-stream@4.1.1: + resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} + engines: {node: '>= 10'} + + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.0.14: + resolution: {integrity: sha512-nGFJTnJN6cM2v9kXL+SOBq3AtjQby3Mv5ySGFof5UGRHrRioSJ5iG680cYNjE/yWk671nROcpPj4hAS8nyLhSw==} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + + zustand@5.0.7: + resolution: {integrity: sha512-Ot6uqHDW/O2VdYsKLLU8GQu8sCOM1LcoE8RwvLv9uuRT9s6SOHCKs0ZEOhxg+I1Ld+A1Q5lwx+UlKXXUoCZITg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + +snapshots: + + 7zip-bin@5.2.0: {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.27.3': {} + + '@babel/core@7.27.4': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helpers': 7.27.4 + '@babel/parser': 7.27.4 + '@babel/template': 7.27.2 + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.3 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/core@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.27.3': + dependencies: + '@babel/parser': 7.27.4 + '@babel/types': 7.27.3 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + + '@babel/generator@7.28.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.28.5 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.27.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.25.0 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.5(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.5) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.5 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.27.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.27.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.28.5 + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.27.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.27.3 + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + + '@babel/parser@7.27.4': + dependencies: + '@babel/types': 7.27.3 + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.5) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-typescript@7.28.5(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.28.5(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.27.1': {} + + '@babel/runtime@7.28.6': {} + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.27.4 + '@babel/types': 7.27.3 + + '@babel/traverse@7.27.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.3 + '@babel/parser': 7.27.4 + '@babel/template': 7.27.2 + '@babel/types': 7.27.3 + debug: 4.4.1 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/traverse@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.27.3': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@biomejs/biome@1.9.4': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 1.9.4 + '@biomejs/cli-darwin-x64': 1.9.4 + '@biomejs/cli-linux-arm64': 1.9.4 + '@biomejs/cli-linux-arm64-musl': 1.9.4 + '@biomejs/cli-linux-x64': 1.9.4 + '@biomejs/cli-linux-x64-musl': 1.9.4 + '@biomejs/cli-win32-arm64': 1.9.4 + '@biomejs/cli-win32-x64': 1.9.4 + + '@biomejs/cli-darwin-arm64@1.9.4': + optional: true + + '@biomejs/cli-darwin-x64@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64@1.9.4': + optional: true + + '@biomejs/cli-linux-x64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-x64@1.9.4': + optional: true + + '@biomejs/cli-win32-arm64@1.9.4': + optional: true + + '@biomejs/cli-win32-x64@1.9.4': + optional: true + + '@clack/core@0.5.0': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.11.0': + dependencies: + '@clack/core': 0.5.0 + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@csstools/postcss-cascade-layers@5.0.2(postcss@8.5.6)': + dependencies: + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.1) + postcss: 8.5.6 + postcss-selector-parser: 7.1.1 + + '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.1)': + dependencies: + postcss-selector-parser: 7.1.1 + + '@develar/schema-utils@2.6.5': + dependencies: + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + + '@dnd-kit/accessibility@3.1.1(react@19.2.4)': + dependencies: + react: 19.2.4 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.2.4) + '@dnd-kit/utilities': 3.2.2(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@dnd-kit/utilities': 3.2.2(react@19.2.4) + react: 19.2.4 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.2.4)': + dependencies: + react: 19.2.4 + tslib: 2.8.1 + + '@electron-forge/cli@6.4.2(encoding@0.1.13)': + dependencies: + '@electron-forge/core': 6.4.2(encoding@0.1.13) + '@electron-forge/shared-types': 6.4.2 + '@electron/get': 2.0.3 + chalk: 4.1.2 + commander: 4.1.1 + debug: 4.4.1 + fs-extra: 10.1.0 + listr2: 5.0.8 + semver: 7.7.2 + transitivePeerDependencies: + - bluebird + - encoding + - enquirer + - supports-color + + '@electron-forge/core-utils@6.4.2': + dependencies: + '@electron-forge/shared-types': 6.4.2 + '@electron/rebuild': 3.7.2 + '@malept/cross-spawn-promise': 2.0.0 + chalk: 4.1.2 + debug: 4.4.1 + find-up: 5.0.0 + fs-extra: 10.1.0 + log-symbols: 4.1.0 + semver: 7.7.2 + yarn-or-npm: 3.0.1 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/core@6.4.2(encoding@0.1.13)': + dependencies: + '@electron-forge/core-utils': 6.4.2 + '@electron-forge/maker-base': 6.4.2 + '@electron-forge/plugin-base': 6.4.2 + '@electron-forge/publisher-base': 6.4.2 + '@electron-forge/shared-types': 6.4.2 + '@electron-forge/template-base': 6.4.2 + '@electron-forge/template-vite': 6.4.2 + '@electron-forge/template-vite-typescript': 6.4.2 + '@electron-forge/template-webpack': 6.4.2 + '@electron-forge/template-webpack-typescript': 6.4.2 + '@electron/get': 2.0.3 + '@electron/rebuild': 3.7.2 + '@malept/cross-spawn-promise': 2.0.0 + chalk: 4.1.2 + debug: 4.4.1 + electron-packager: 17.1.2 + fast-glob: 3.3.3 + filenamify: 4.3.0 + find-up: 5.0.0 + fs-extra: 10.1.0 + got: 11.8.6 + interpret: 3.1.1 + listr2: 5.0.8 + lodash: 4.17.21 + log-symbols: 4.1.0 + node-fetch: 2.7.0(encoding@0.1.13) + progress: 2.0.3 + rechoir: 0.8.0 + resolve-package: 1.0.1 + semver: 7.7.2 + source-map-support: 0.5.21 + sudo-prompt: 9.2.1 + username: 5.1.0 + yarn-or-npm: 3.0.1 + transitivePeerDependencies: + - bluebird + - encoding + - enquirer + - supports-color + + '@electron-forge/maker-base@6.4.2': + dependencies: + '@electron-forge/shared-types': 6.4.2 + fs-extra: 10.1.0 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/maker-deb@6.4.2': + dependencies: + '@electron-forge/maker-base': 6.4.2 + '@electron-forge/shared-types': 6.4.2 + optionalDependencies: + electron-installer-debian: 3.2.0 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/maker-rpm@6.4.2': + dependencies: + '@electron-forge/maker-base': 6.4.2 + '@electron-forge/shared-types': 6.4.2 + optionalDependencies: + electron-installer-redhat: 3.4.0 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/maker-squirrel@6.4.2': + dependencies: + '@electron-forge/maker-base': 6.4.2 + '@electron-forge/shared-types': 6.4.2 + fs-extra: 10.1.0 + optionalDependencies: + electron-winstaller: 5.4.0 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/maker-zip@6.4.2': + dependencies: + '@electron-forge/maker-base': 6.4.2 + '@electron-forge/shared-types': 6.4.2 + cross-zip: 4.0.1 + fs-extra: 10.1.0 + got: 11.8.6 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/plugin-base@6.4.2': + dependencies: + '@electron-forge/shared-types': 6.4.2 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/publisher-base@6.4.2': + dependencies: + '@electron-forge/shared-types': 6.4.2 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/shared-types@6.4.2': + dependencies: + '@electron/rebuild': 3.7.2 + electron-packager: 17.1.2 + listr2: 5.0.8 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/template-base@6.4.2': + dependencies: + '@electron-forge/shared-types': 6.4.2 + '@malept/cross-spawn-promise': 2.0.0 + debug: 4.4.1 + fs-extra: 10.1.0 + username: 5.1.0 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/template-vite-typescript@6.4.2': + dependencies: + '@electron-forge/shared-types': 6.4.2 + '@electron-forge/template-base': 6.4.2 + fs-extra: 10.1.0 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/template-vite@6.4.2': + dependencies: + '@electron-forge/shared-types': 6.4.2 + '@electron-forge/template-base': 6.4.2 + fs-extra: 10.1.0 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/template-webpack-typescript@6.4.2': + dependencies: + '@electron-forge/shared-types': 6.4.2 + '@electron-forge/template-base': 6.4.2 + fs-extra: 10.1.0 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/template-webpack@6.4.2': + dependencies: + '@electron-forge/shared-types': 6.4.2 + '@electron-forge/template-base': 6.4.2 + fs-extra: 10.1.0 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-toolkit/preload@3.0.2(electron@22.3.27)': + dependencies: + electron: 22.3.27 + + '@electron-toolkit/tsconfig@1.0.1(@types/node@20.19.1)': + dependencies: + '@types/node': 20.19.1 + + '@electron-toolkit/utils@4.0.0(electron@22.3.27)': + dependencies: + electron: 22.3.27 + + '@electron/asar@3.2.18': + dependencies: + commander: 5.1.0 + glob: 7.2.3 + minimatch: 3.1.2 + + '@electron/asar@3.4.1': + dependencies: + commander: 5.1.0 + glob: 7.2.3 + minimatch: 3.1.2 + + '@electron/fuses@1.8.0': + dependencies: + chalk: 4.1.2 + fs-extra: 9.1.0 + minimist: 1.2.8 + + '@electron/get@2.0.3': + dependencies: + debug: 4.4.1 + env-paths: 2.2.1 + fs-extra: 8.1.0 + got: 11.8.6 + progress: 2.0.3 + semver: 6.3.1 + sumchecker: 3.0.1 + optionalDependencies: + global-agent: 3.0.0 + transitivePeerDependencies: + - supports-color + + '@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2': + dependencies: + env-paths: 2.2.1 + exponential-backoff: 3.1.3 + glob: 8.1.0 + graceful-fs: 4.2.11 + make-fetch-happen: 10.2.1 + nopt: 6.0.0 + proc-log: 2.0.1 + semver: 7.7.2 + tar: 6.2.1 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - supports-color + + '@electron/notarize@1.2.4': + dependencies: + debug: 4.4.1 + fs-extra: 9.1.0 + transitivePeerDependencies: + - supports-color + + '@electron/notarize@2.5.0': + dependencies: + debug: 4.4.1 + fs-extra: 9.1.0 + promise-retry: 2.0.1 + transitivePeerDependencies: + - supports-color + + '@electron/osx-sign@1.3.1': + dependencies: + compare-version: 0.1.2 + debug: 4.4.1 + fs-extra: 10.1.0 + isbinaryfile: 4.0.10 + minimist: 1.2.8 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@electron/osx-sign@1.3.3': + dependencies: + compare-version: 0.1.2 + debug: 4.4.1 + fs-extra: 10.1.0 + isbinaryfile: 4.0.10 + minimist: 1.2.8 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@electron/rebuild@3.7.0': + dependencies: + '@electron/node-gyp': https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2 + '@malept/cross-spawn-promise': 2.0.0 + chalk: 4.1.2 + debug: 4.4.1 + detect-libc: 2.0.4 + fs-extra: 10.1.0 + got: 11.8.6 + node-abi: 3.75.0 + node-api-version: 0.2.1 + ora: 5.4.1 + read-binary-file-arch: 1.0.6 + semver: 7.7.2 + tar: 6.2.1 + yargs: 17.7.2 + transitivePeerDependencies: + - bluebird + - supports-color + + '@electron/rebuild@3.7.2': + dependencies: + '@electron/node-gyp': https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2 + '@malept/cross-spawn-promise': 2.0.0 + chalk: 4.1.2 + debug: 4.4.1 + detect-libc: 2.0.4 + fs-extra: 10.1.0 + got: 11.8.6 + node-abi: 3.75.0 + node-api-version: 0.2.1 + ora: 5.4.1 + read-binary-file-arch: 1.0.6 + semver: 7.7.2 + tar: 6.2.1 + yargs: 17.7.2 + transitivePeerDependencies: + - bluebird + - supports-color + + '@electron/universal@1.5.1': + dependencies: + '@electron/asar': 3.4.1 + '@malept/cross-spawn-promise': 1.1.1 + debug: 4.4.1 + dir-compare: 3.3.0 + fs-extra: 9.1.0 + minimatch: 3.1.2 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@electron/universal@2.0.1': + dependencies: + '@electron/asar': 3.4.1 + '@malept/cross-spawn-promise': 2.0.0 + debug: 4.4.1 + dir-compare: 4.2.0 + fs-extra: 11.3.0 + minimatch: 9.0.5 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@electron/windows-sign@1.2.2': + dependencies: + cross-dirname: 0.1.0 + debug: 4.4.1 + fs-extra: 11.3.0 + minimist: 1.2.8 + postject: 1.0.0-alpha.6 + transitivePeerDependencies: + - supports-color + optional: true + + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.5': + optional: true + + '@esbuild/android-arm64@0.25.5': + optional: true + + '@esbuild/android-arm@0.25.5': + optional: true + + '@esbuild/android-x64@0.25.5': + optional: true + + '@esbuild/darwin-arm64@0.25.5': + optional: true + + '@esbuild/darwin-x64@0.25.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.5': + optional: true + + '@esbuild/freebsd-x64@0.25.5': + optional: true + + '@esbuild/linux-arm64@0.25.5': + optional: true + + '@esbuild/linux-arm@0.25.5': + optional: true + + '@esbuild/linux-ia32@0.25.5': + optional: true + + '@esbuild/linux-loong64@0.25.5': + optional: true + + '@esbuild/linux-mips64el@0.25.5': + optional: true + + '@esbuild/linux-ppc64@0.25.5': + optional: true + + '@esbuild/linux-riscv64@0.25.5': + optional: true + + '@esbuild/linux-s390x@0.25.5': + optional: true + + '@esbuild/linux-x64@0.25.5': + optional: true + + '@esbuild/netbsd-arm64@0.25.5': + optional: true + + '@esbuild/netbsd-x64@0.25.5': + optional: true + + '@esbuild/openbsd-arm64@0.25.5': + optional: true + + '@esbuild/openbsd-x64@0.25.5': + optional: true + + '@esbuild/sunos-x64@0.25.5': + optional: true + + '@esbuild/win32-arm64@0.25.5': + optional: true + + '@esbuild/win32-ia32@0.25.5': + optional: true + + '@esbuild/win32-x64@0.25.5': + optional: true + + '@fast-csv/format@4.3.5': + dependencies: + '@types/node': 14.18.63 + lodash.escaperegexp: 4.1.2 + lodash.isboolean: 3.0.3 + lodash.isequal: 4.5.0 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + + '@fast-csv/parse@4.3.6': + dependencies: + '@types/node': 14.18.63 + lodash.escaperegexp: 4.1.2 + lodash.groupby: 4.6.0 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + lodash.isundefined: 3.0.1 + lodash.uniq: 4.5.0 + + '@floating-ui/core@1.7.4': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.5': + dependencies: + '@floating-ui/core': 1.7.4 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/dom': 1.7.5 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@floating-ui/utils@0.2.10': {} + + '@gar/promisify@1.1.3': {} + + '@hapi/hoek@9.3.0': {} + + '@hapi/topo@5.1.0': + dependencies: + '@hapi/hoek': 9.3.0 + + '@hono/node-server@1.19.9(hono@4.11.7)': + dependencies: + hono: 4.11.7 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@malept/cross-spawn-promise@1.1.1': + dependencies: + cross-spawn: 7.0.6 + + '@malept/cross-spawn-promise@2.0.0': + dependencies: + cross-spawn: 7.0.6 + + '@malept/flatpak-bundler@0.4.0': + dependencies: + debug: 4.4.1 + fs-extra: 9.1.0 + lodash: 4.17.21 + tmp-promise: 3.0.3 + transitivePeerDependencies: + - supports-color + + '@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.0.14)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.11.7) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 7.5.1(express@5.2.1) + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.0.14 + zod-to-json-schema: 3.25.1(zod@4.0.14) + transitivePeerDependencies: + - hono + - supports-color + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@npmcli/fs@2.1.2': + dependencies: + '@gar/promisify': 1.1.3 + semver: 7.7.2 + + '@npmcli/move-file@2.0.1': + dependencies: + mkdirp: 1.0.4 + rimraf: 3.0.2 + + '@oxc-parser/binding-android-arm-eabi@0.127.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.127.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + optional: true + + '@oxc-project/types@0.127.0': {} + + '@oxc-resolver/binding-android-arm-eabi@11.19.1': + optional: true + + '@oxc-resolver/binding-android-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.19.1': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.19.1': + optional: true + + '@oxc-resolver/binding-win32-ia32-msvc@11.19.1': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.19.1': + optional: true + + '@pandacss/config@1.8.1': + dependencies: + '@pandacss/logger': 1.8.1 + '@pandacss/preset-base': 1.8.1 + '@pandacss/preset-panda': 1.8.1 + '@pandacss/shared': 1.8.1 + '@pandacss/types': 1.8.1 + bundle-n-require: 1.1.2 + escalade: 3.2.0 + microdiff: 1.5.0 + typescript: 5.9.3 + + '@pandacss/core@1.8.1': + dependencies: + '@csstools/postcss-cascade-layers': 5.0.2(postcss@8.5.6) + '@pandacss/is-valid-prop': 1.8.1 + '@pandacss/logger': 1.8.1 + '@pandacss/shared': 1.8.1 + '@pandacss/token-dictionary': 1.8.1 + '@pandacss/types': 1.8.1 + browserslist: 4.28.0 + hookable: 5.5.3 + lightningcss: 1.30.2 + lodash.merge: 4.6.2 + outdent: 0.8.0 + postcss: 8.5.6 + postcss-discard-duplicates: 7.0.2(postcss@8.5.6) + postcss-discard-empty: 7.0.1(postcss@8.5.6) + postcss-merge-rules: 7.0.7(postcss@8.5.6) + postcss-minify-selectors: 7.0.5(postcss@8.5.6) + postcss-nested: 7.0.2(postcss@8.5.6) + postcss-normalize-whitespace: 7.0.1(postcss@8.5.6) + postcss-selector-parser: 7.1.1 + ts-pattern: 5.9.0 + + '@pandacss/dev@1.8.1(hono@4.11.7)(typescript@5.9.2)': + dependencies: + '@clack/prompts': 0.11.0 + '@pandacss/config': 1.8.1 + '@pandacss/logger': 1.8.1 + '@pandacss/mcp': 1.8.1(hono@4.11.7)(typescript@5.9.2) + '@pandacss/node': 1.8.1(typescript@5.9.2) + '@pandacss/postcss': 1.8.1(typescript@5.9.2) + '@pandacss/preset-base': 1.8.1 + '@pandacss/preset-panda': 1.8.1 + '@pandacss/shared': 1.8.1 + '@pandacss/token-dictionary': 1.8.1 + '@pandacss/types': 1.8.1 + cac: 6.7.14 + transitivePeerDependencies: + - '@cfworker/json-schema' + - hono + - jsdom + - supports-color + - typescript + + '@pandacss/extractor@1.8.1(typescript@5.9.2)': + dependencies: + '@pandacss/shared': 1.8.1 + ts-evaluator: 1.2.0(typescript@5.9.2) + ts-morph: 27.0.2 + transitivePeerDependencies: + - jsdom + - typescript + + '@pandacss/generator@1.8.1': + dependencies: + '@pandacss/core': 1.8.1 + '@pandacss/is-valid-prop': 1.8.1 + '@pandacss/logger': 1.8.1 + '@pandacss/shared': 1.8.1 + '@pandacss/token-dictionary': 1.8.1 + '@pandacss/types': 1.8.1 + javascript-stringify: 2.1.0 + outdent: 0.8.0 + pluralize: 8.0.0 + postcss: 8.5.6 + ts-pattern: 5.9.0 + + '@pandacss/is-valid-prop@1.8.1': {} + + '@pandacss/logger@1.8.1': + dependencies: + '@pandacss/types': 1.8.1 + kleur: 4.1.5 + + '@pandacss/mcp@1.8.1(hono@4.11.7)(typescript@5.9.2)': + dependencies: + '@clack/prompts': 0.11.0 + '@modelcontextprotocol/sdk': 1.25.3(hono@4.11.7)(zod@4.0.14) + '@pandacss/logger': 1.8.1 + '@pandacss/node': 1.8.1(typescript@5.9.2) + '@pandacss/token-dictionary': 1.8.1 + '@pandacss/types': 1.8.1 + zod: 4.0.14 + transitivePeerDependencies: + - '@cfworker/json-schema' + - hono + - jsdom + - supports-color + - typescript + + '@pandacss/node@1.8.1(typescript@5.9.2)': + dependencies: + '@pandacss/config': 1.8.1 + '@pandacss/core': 1.8.1 + '@pandacss/generator': 1.8.1 + '@pandacss/logger': 1.8.1 + '@pandacss/parser': 1.8.1(typescript@5.9.2) + '@pandacss/reporter': 1.8.1 + '@pandacss/shared': 1.8.1 + '@pandacss/token-dictionary': 1.8.1 + '@pandacss/types': 1.8.1 + browserslist: 4.28.0 + chokidar: 4.0.3 + fast-glob: 3.3.3 + fs-extra: 11.3.2 + glob-parent: 6.0.2 + is-glob: 4.0.3 + lodash.merge: 4.6.2 + look-it-up: 2.1.0 + outdent: 0.8.0 + p-limit: 5.0.0 + package-manager-detector: 1.6.0 + perfect-debounce: 1.0.0 + picomatch: 4.0.3 + pkg-types: 2.3.0 + pluralize: 8.0.0 + postcss: 8.5.6 + prettier: 3.2.5 + ts-morph: 27.0.2 + ts-pattern: 5.9.0 + tsconfck: 3.1.6(typescript@5.9.2) + transitivePeerDependencies: + - jsdom + - typescript + + '@pandacss/parser@1.8.1(typescript@5.9.2)': + dependencies: + '@pandacss/config': 1.8.1 + '@pandacss/core': 1.8.1 + '@pandacss/extractor': 1.8.1(typescript@5.9.2) + '@pandacss/logger': 1.8.1 + '@pandacss/shared': 1.8.1 + '@pandacss/types': 1.8.1 + '@vue/compiler-sfc': 3.5.25 + magic-string: 0.30.21 + ts-morph: 27.0.2 + ts-pattern: 5.9.0 + transitivePeerDependencies: + - jsdom + - typescript + + '@pandacss/postcss@1.8.1(typescript@5.9.2)': + dependencies: + '@pandacss/node': 1.8.1(typescript@5.9.2) + postcss: 8.5.6 + transitivePeerDependencies: + - jsdom + - typescript + + '@pandacss/preset-base@1.8.1': + dependencies: + '@pandacss/types': 1.8.1 + + '@pandacss/preset-panda@1.8.1': + dependencies: + '@pandacss/types': 1.8.1 + + '@pandacss/reporter@1.8.1': + dependencies: + '@pandacss/core': 1.8.1 + '@pandacss/generator': 1.8.1 + '@pandacss/logger': 1.8.1 + '@pandacss/shared': 1.8.1 + '@pandacss/types': 1.8.1 + table: 6.9.0 + wordwrapjs: 5.1.1 + + '@pandacss/shared@1.8.1': {} + + '@pandacss/token-dictionary@1.8.1': + dependencies: + '@pandacss/logger': 1.8.1 + '@pandacss/shared': 1.8.1 + '@pandacss/types': 1.8.1 + picomatch: 4.0.3 + ts-pattern: 5.9.0 + + '@pandacss/types@1.8.1': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.13)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-context@1.1.2(@types/react@19.2.13)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.13)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.13)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.13)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-id@1.1.1(@types/react@19.2.13)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.13)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/react-dom': 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/rect': 1.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.13)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.13)(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.13)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.13)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.13)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.13)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.13)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.13)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.13)(react@19.2.4)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.13)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/rect@1.1.1': {} + + '@rolldown/pluginutils@1.0.0-beta.9': {} + + '@rollup/rollup-android-arm-eabi@4.41.1': + optional: true + + '@rollup/rollup-android-arm-eabi@4.53.3': + optional: true + + '@rollup/rollup-android-arm64@4.41.1': + optional: true + + '@rollup/rollup-android-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.41.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-x64@4.41.1': + optional: true + + '@rollup/rollup-darwin-x64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.41.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.41.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.41.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.41.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.41.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.41.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.41.1': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.41.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.41.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.41.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.41.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.41.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.41.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.53.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.53.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.41.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.41.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.41.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.53.3': + optional: true + + '@sideway/address@4.1.5': + dependencies: + '@hapi/hoek': 9.3.0 + + '@sideway/formula@3.0.1': {} + + '@sideway/pinpoint@2.0.0': {} + + '@sindresorhus/is@4.6.0': {} + + '@standard-schema/spec@1.1.0': {} + + '@stitches/react@1.2.8(react@19.2.4)': + dependencies: + react: 19.2.4 + + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + + '@tanstack/form-core@1.11.0': + dependencies: + '@tanstack/store': 0.7.0 + + '@tanstack/history@1.139.0': {} + + '@tanstack/query-core@5.100.5': {} + + '@tanstack/react-form@1.11.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/form-core': 1.11.0 + '@tanstack/react-store': 0.7.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + decode-formdata: 0.9.0 + devalue: 5.1.1 + react: 19.2.4 + transitivePeerDependencies: + - react-dom + + '@tanstack/react-query@5.100.5(react@19.2.4)': + dependencies: + '@tanstack/query-core': 5.100.5 + react: 19.2.4 + + '@tanstack/react-router-devtools@1.139.12(@tanstack/react-router@1.139.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.139.12)(@types/node@20.19.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.10)(tsx@4.20.6)(yaml@2.8.3)': + dependencies: + '@tanstack/react-router': 1.139.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/router-devtools-core': 1.139.12(@tanstack/router-core@1.139.12)(@types/node@20.19.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(solid-js@1.9.10)(tsx@4.20.6)(yaml@2.8.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + vite: 7.2.4(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3) + optionalDependencies: + '@tanstack/router-core': 1.139.12 + transitivePeerDependencies: + - '@types/node' + - csstype + - jiti + - less + - lightningcss + - sass + - sass-embedded + - solid-js + - stylus + - sugarss + - terser + - tsx + - yaml + + '@tanstack/react-router@1.139.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/history': 1.139.0 + '@tanstack/react-store': 0.8.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/router-core': 1.139.12 + isbot: 5.1.32 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/react-store@0.7.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/store': 0.7.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + use-sync-external-store: 1.5.0(react@19.2.4) + + '@tanstack/react-store@0.8.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/store': 0.8.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) + + '@tanstack/router-core@1.139.12': + dependencies: + '@tanstack/history': 1.139.0 + '@tanstack/store': 0.8.0 + cookie-es: 2.0.0 + seroval: 1.4.0 + seroval-plugins: 1.4.0(seroval@1.4.0) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/router-devtools-core@1.139.12(@tanstack/router-core@1.139.12)(@types/node@20.19.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(solid-js@1.9.10)(tsx@4.20.6)(yaml@2.8.3)': + dependencies: + '@tanstack/router-core': 1.139.12 + clsx: 2.1.1 + goober: 2.1.18(csstype@3.2.3) + solid-js: 1.9.10 + tiny-invariant: 1.3.3 + vite: 7.2.4(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3) + optionalDependencies: + csstype: 3.2.3 + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + '@tanstack/router-generator@1.139.12': + dependencies: + '@tanstack/router-core': 1.139.12 + '@tanstack/router-utils': 1.139.0 + '@tanstack/virtual-file-routes': 1.139.0 + prettier: 3.7.3 + recast: 0.23.11 + source-map: 0.7.6 + tsx: 4.20.6 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@tanstack/router-plugin@1.139.12(@tanstack/react-router@1.139.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@tanstack/router-core': 1.139.12 + '@tanstack/router-generator': 1.139.12 + '@tanstack/router-utils': 1.139.0 + '@tanstack/virtual-file-routes': 1.139.0 + babel-dead-code-elimination: 1.0.10 + chokidar: 3.6.0 + unplugin: 2.3.11 + zod: 3.25.76 + optionalDependencies: + '@tanstack/react-router': 1.139.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + vite: 6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3) + transitivePeerDependencies: + - supports-color + + '@tanstack/router-utils@1.139.0': + dependencies: + '@babel/core': 7.28.5 + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) + ansis: 4.2.0 + diff: 8.0.2 + pathe: 2.0.3 + tinyglobby: 0.2.15 + transitivePeerDependencies: + - supports-color + + '@tanstack/store@0.7.0': {} + + '@tanstack/store@0.8.0': {} + + '@tanstack/virtual-file-routes@1.139.0': {} + + '@tootallnate/once@2.0.0': {} + + '@ts-morph/common@0.28.1': + dependencies: + minimatch: 10.0.1 + path-browserify: 1.0.1 + tinyglobby: 0.2.15 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.27.4 + '@babel/types': 7.27.3 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.7 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.27.3 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.27.4 + '@babel/types': 7.27.3 + + '@types/babel__traverse@7.20.7': + dependencies: + '@babel/types': 7.27.3 + + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.0.4 + '@types/keyv': 3.1.4 + '@types/node': 16.18.126 + '@types/responselike': 1.0.3 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.7': {} + + '@types/estree@1.0.8': {} + + '@types/fs-extra@9.0.13': + dependencies: + '@types/node': 16.18.126 + + '@types/http-cache-semantics@4.0.4': {} + + '@types/keyv@3.1.4': + dependencies: + '@types/node': 16.18.126 + + '@types/ms@2.1.0': {} + + '@types/node@14.18.63': {} + + '@types/node@16.18.126': {} + + '@types/node@17.0.45': {} + + '@types/node@20.19.1': + dependencies: + undici-types: 6.21.0 + + '@types/plist@3.0.5': + dependencies: + '@types/node': 16.18.126 + xmlbuilder: 15.1.1 + optional: true + + '@types/react-dom@19.2.3(@types/react@19.2.13)': + dependencies: + '@types/react': 19.2.13 + + '@types/react@19.2.13': + dependencies: + csstype: 3.2.3 + + '@types/regex-escape@3.4.1': {} + + '@types/responselike@1.0.3': + dependencies: + '@types/node': 16.18.126 + + '@types/verror@1.10.11': + optional: true + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 16.18.126 + optional: true + + '@vitejs/plugin-react@4.5.0(vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3))': + dependencies: + '@babel/core': 7.27.4 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.4) + '@rolldown/pluginutils': 1.0.0-beta.9 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@4.1.6': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.6 + '@vitest/utils': 4.1.6 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.6(vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.6 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3) + + '@vitest/pretty-format@4.1.6': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.6': + dependencies: + '@vitest/utils': 4.1.6 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.6': + dependencies: + '@vitest/pretty-format': 4.1.6 + '@vitest/utils': 4.1.6 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.6': {} + + '@vitest/utils@4.1.6': + dependencies: + '@vitest/pretty-format': 4.1.6 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + '@vue/compiler-core@3.5.25': + dependencies: + '@babel/parser': 7.28.5 + '@vue/shared': 3.5.25 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.25': + dependencies: + '@vue/compiler-core': 3.5.25 + '@vue/shared': 3.5.25 + + '@vue/compiler-sfc@3.5.25': + dependencies: + '@babel/parser': 7.28.5 + '@vue/compiler-core': 3.5.25 + '@vue/compiler-dom': 3.5.25 + '@vue/compiler-ssr': 3.5.25 + '@vue/shared': 3.5.25 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.25': + dependencies: + '@vue/compiler-dom': 3.5.25 + '@vue/shared': 3.5.25 + + '@vue/shared@3.5.25': {} + + '@xmldom/xmldom@0.8.10': {} + + abbrev@1.1.1: {} + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn@8.15.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + agent-base@7.1.3: {} + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv-keywords@3.5.2(ajv@6.12.6): + dependencies: + ajv: 6.12.6 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-colors@4.1.3: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-escapes@5.0.0: + dependencies: + type-fest: 1.4.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + ansis@4.2.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + app-builder-bin@5.0.0-alpha.12: {} + + app-builder-lib@26.0.12(dmg-builder@26.0.12)(electron-builder-squirrel-windows@26.0.12): + dependencies: + '@develar/schema-utils': 2.6.5 + '@electron/asar': 3.2.18 + '@electron/fuses': 1.8.0 + '@electron/notarize': 2.5.0 + '@electron/osx-sign': 1.3.1 + '@electron/rebuild': 3.7.0 + '@electron/universal': 2.0.1 + '@malept/flatpak-bundler': 0.4.0 + '@types/fs-extra': 9.0.13 + async-exit-hook: 2.0.1 + builder-util: 26.0.11 + builder-util-runtime: 9.3.1 + chromium-pickle-js: 0.2.0 + config-file-ts: 0.2.8-rc1 + debug: 4.4.1 + dmg-builder: 26.0.12(electron-builder-squirrel-windows@26.0.12) + dotenv: 16.5.0 + dotenv-expand: 11.0.7 + ejs: 3.1.10 + electron-builder-squirrel-windows: 26.0.12(dmg-builder@26.0.12) + electron-publish: 26.0.11 + fs-extra: 10.1.0 + hosted-git-info: 4.1.0 + is-ci: 3.0.1 + isbinaryfile: 5.0.4 + js-yaml: 4.1.0 + json5: 2.2.3 + lazy-val: 1.0.5 + minimatch: 10.0.1 + plist: 3.1.0 + resedit: 1.7.2 + semver: 7.7.2 + tar: 6.2.1 + temp-file: 3.4.0 + tiny-async-pool: 1.3.0 + transitivePeerDependencies: + - bluebird + - supports-color + + archiver-utils@2.1.0: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 2.3.8 + + archiver-utils@3.0.4: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + archiver@5.3.2: + dependencies: + archiver-utils: 2.1.0 + async: 3.2.6 + buffer-crc32: 0.2.13 + readable-stream: 3.6.2 + readdir-glob: 1.1.3 + tar-stream: 2.2.0 + zip-stream: 4.1.1 + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + assert-plus@1.0.0: + optional: true + + assertion-error@2.0.1: {} + + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + + astral-regex@2.0.0: {} + + async-exit-hook@2.0.1: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + + author-regex@1.0.0: {} + + axios@0.25.0: + dependencies: + follow-redirects: 1.15.9 + transitivePeerDependencies: + - debug + + axios@1.13.4: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + babel-dead-code-elimination@1.0.10: + dependencies: + '@babel/core': 7.28.5 + '@babel/parser': 7.27.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.9.19: {} + + big-integer@1.6.52: {} + + binary-extensions@2.3.0: {} + + binary@0.3.0: + dependencies: + buffers: 0.1.1 + chainsaw: 0.1.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bluebird@3.4.7: {} + + bluebird@3.7.2: {} + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.14.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + boolean@3.2.0: + optional: true + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@2.1.0: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.25.0: + dependencies: + caniuse-lite: 1.0.30001720 + electron-to-chromium: 1.5.161 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.0) + + browserslist@4.28.0: + dependencies: + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001766 + electron-to-chromium: 1.5.283 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.0) + + buffer-crc32@0.2.13: {} + + buffer-equal@1.0.1: {} + + buffer-from@1.1.2: {} + + buffer-indexof-polyfill@1.0.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buffers@0.1.1: {} + + builder-util-runtime@9.3.1: + dependencies: + debug: 4.4.1 + sax: 1.4.1 + transitivePeerDependencies: + - supports-color + + builder-util@26.0.11: + dependencies: + 7zip-bin: 5.2.0 + '@types/debug': 4.1.12 + app-builder-bin: 5.0.0-alpha.12 + builder-util-runtime: 9.3.1 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + fs-extra: 10.1.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-ci: 3.0.1 + js-yaml: 4.1.0 + sanitize-filename: 1.6.3 + source-map-support: 0.5.21 + stat-mode: 1.0.0 + temp-file: 3.4.0 + tiny-async-pool: 1.3.0 + transitivePeerDependencies: + - supports-color + + bundle-n-require@1.1.2: + dependencies: + esbuild: 0.25.5 + node-eval: 2.0.0 + + bytes@3.1.2: {} + + cac@6.7.14: {} + + cacache@16.1.3: + dependencies: + '@npmcli/fs': 2.1.2 + '@npmcli/move-file': 2.0.1 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 8.1.0 + infer-owner: 1.0.4 + lru-cache: 7.18.3 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1 + rimraf: 3.0.2 + ssri: 9.0.1 + tar: 6.2.1 + unique-filename: 2.0.1 + transitivePeerDependencies: + - bluebird + + cacheable-lookup@5.0.4: {} + + cacheable-request@7.0.4: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + caniuse-api@3.0.0: + dependencies: + browserslist: 4.28.0 + caniuse-lite: 1.0.30001720 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 + + caniuse-lite@1.0.30001720: {} + + caniuse-lite@1.0.30001766: {} + + chai@6.2.2: {} + + chainsaw@0.1.0: + dependencies: + traverse: 0.3.9 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.3.0: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chownr@2.0.0: {} + + chromium-pickle-js@0.2.0: {} + + ci-info@3.9.0: {} + + clean-stack@2.2.0: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-cursor@4.0.0: + dependencies: + restore-cursor: 4.0.0 + + cli-spinners@2.9.2: {} + + cli-truncate@2.1.0: + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + + cli-truncate@3.1.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 5.1.2 + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + optional: true + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + + clone@1.0.4: {} + + clsx@2.1.1: {} + + code-block-writer@13.0.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@11.0.0: {} + + commander@4.1.1: {} + + commander@5.1.0: {} + + commander@9.4.1: {} + + commander@9.5.0: + optional: true + + compare-version@0.1.2: {} + + compress-commons@4.1.2: + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 4.0.3 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + concat-map@0.0.1: {} + + concurrently@7.6.0: + dependencies: + chalk: 4.1.2 + date-fns: 2.30.0 + lodash: 4.17.21 + rxjs: 7.8.2 + shell-quote: 1.8.2 + spawn-command: 0.0.2 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + + confbox@0.2.2: {} + + config-file-ts@0.2.8-rc1: + dependencies: + glob: 10.4.5 + typescript: 5.9.2 + + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-es@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + core-util-is@1.0.2: + optional: true + + core-util-is@1.0.3: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + crc-32@1.2.2: {} + + crc32-stream@4.0.3: + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.2 + + crc@3.8.0: + dependencies: + buffer: 5.7.1 + optional: true + + cross-dirname@0.1.0: + optional: true + + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + + cross-spawn-windows-exe@1.2.0: + dependencies: + '@malept/cross-spawn-promise': 1.1.1 + is-wsl: 2.2.0 + which: 2.0.2 + + cross-spawn@6.0.6: + dependencies: + nice-try: 1.0.5 + path-key: 2.0.1 + semver: 5.7.2 + shebang-command: 1.2.0 + which: 1.3.1 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cross-zip@4.0.1: {} + + crosspath@2.0.0: + dependencies: + '@types/node': 17.0.45 + + cssesc@3.0.0: {} + + cssnano-utils@5.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + csstype@3.2.3: {} + + date-fns@2.30.0: + dependencies: + '@babel/runtime': 7.27.1 + + dayjs@1.11.13: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@3.2.7(supports-color@5.5.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-formdata@0.9.0: {} + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + defer-to-connect@2.0.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + optional: true + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + optional: true + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + detect-libc@2.0.4: {} + + detect-node-es@1.1.0: {} + + detect-node@2.1.0: + optional: true + + devalue@5.1.1: {} + + diff@8.0.2: {} + + dir-compare@3.3.0: + dependencies: + buffer-equal: 1.0.1 + minimatch: 3.1.2 + + dir-compare@4.2.0: + dependencies: + minimatch: 3.1.2 + p-limit: 3.1.0 + + dmg-builder@26.0.12(electron-builder-squirrel-windows@26.0.12): + dependencies: + app-builder-lib: 26.0.12(dmg-builder@26.0.12)(electron-builder-squirrel-windows@26.0.12) + builder-util: 26.0.11 + builder-util-runtime: 9.3.1 + fs-extra: 10.1.0 + iconv-lite: 0.6.3 + js-yaml: 4.1.0 + optionalDependencies: + dmg-license: 1.0.11 + transitivePeerDependencies: + - bluebird + - electron-builder-squirrel-windows + - supports-color + + dmg-license@1.0.11: + dependencies: + '@types/plist': 3.0.5 + '@types/verror': 1.10.11 + ajv: 6.12.6 + crc: 3.8.0 + iconv-corefoundation: 1.1.7 + plist: 3.1.0 + smart-buffer: 4.2.0 + verror: 1.10.1 + optional: true + + dotenv-expand@11.0.7: + dependencies: + dotenv: 16.5.0 + + dotenv@16.5.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + + eastasianwidth@0.2.0: {} + + ee-first@1.1.1: {} + + ejs@3.1.10: + dependencies: + jake: 10.9.2 + + electron-builder-squirrel-windows@26.0.12(dmg-builder@26.0.12): + dependencies: + app-builder-lib: 26.0.12(dmg-builder@26.0.12)(electron-builder-squirrel-windows@26.0.12) + builder-util: 26.0.11 + electron-winstaller: 5.4.0 + transitivePeerDependencies: + - bluebird + - dmg-builder + - supports-color + + electron-builder@26.0.12(electron-builder-squirrel-windows@26.0.12): + dependencies: + app-builder-lib: 26.0.12(dmg-builder@26.0.12)(electron-builder-squirrel-windows@26.0.12) + builder-util: 26.0.11 + builder-util-runtime: 9.3.1 + chalk: 4.1.2 + dmg-builder: 26.0.12(electron-builder-squirrel-windows@26.0.12) + fs-extra: 10.1.0 + is-ci: 3.0.1 + lazy-val: 1.0.5 + simple-update-notifier: 2.0.0 + yargs: 17.7.2 + transitivePeerDependencies: + - bluebird + - electron-builder-squirrel-windows + - supports-color + + electron-debug@3.2.0: + dependencies: + electron-is-dev: 1.2.0 + electron-localshortcut: 3.2.1 + transitivePeerDependencies: + - supports-color + + electron-devtools-installer@3.2.1: + dependencies: + rimraf: 3.0.2 + semver: 7.7.2 + tslib: 2.8.1 + unzip-crx-3: 0.2.0 + + electron-installer-common@0.10.4: + dependencies: + '@electron/asar': 3.4.1 + '@malept/cross-spawn-promise': 1.1.1 + debug: 4.4.1 + fs-extra: 9.1.0 + glob: 7.2.3 + lodash: 4.17.21 + parse-author: 2.0.0 + semver: 7.7.2 + tmp-promise: 3.0.3 + optionalDependencies: + '@types/fs-extra': 9.0.13 + transitivePeerDependencies: + - supports-color + optional: true + + electron-installer-debian@3.2.0: + dependencies: + '@malept/cross-spawn-promise': 1.1.1 + debug: 4.4.1 + electron-installer-common: 0.10.4 + fs-extra: 9.1.0 + get-folder-size: 2.0.1 + lodash: 4.17.21 + word-wrap: 1.2.5 + yargs: 16.2.0 + transitivePeerDependencies: + - supports-color + optional: true + + electron-installer-redhat@3.4.0: + dependencies: + '@malept/cross-spawn-promise': 1.1.1 + debug: 4.4.1 + electron-installer-common: 0.10.4 + fs-extra: 9.1.0 + lodash: 4.17.21 + word-wrap: 1.2.5 + yargs: 16.2.0 + transitivePeerDependencies: + - supports-color + optional: true + + electron-is-accelerator@0.1.2: {} + + electron-is-dev@1.2.0: {} + + electron-localshortcut@3.2.1: + dependencies: + debug: 4.4.1 + electron-is-accelerator: 0.1.2 + keyboardevent-from-electron-accelerator: 2.0.0 + keyboardevents-areequal: 0.2.2 + transitivePeerDependencies: + - supports-color + + electron-packager@17.1.2: + dependencies: + '@electron/asar': 3.4.1 + '@electron/get': 2.0.3 + '@electron/notarize': 1.2.4 + '@electron/osx-sign': 1.3.3 + '@electron/universal': 1.5.1 + cross-spawn-windows-exe: 1.2.0 + debug: 4.4.1 + extract-zip: 2.0.1 + filenamify: 4.3.0 + fs-extra: 11.3.0 + galactus: 1.0.0 + get-package-info: 1.0.0 + junk: 3.1.0 + parse-author: 2.0.0 + plist: 3.1.0 + rcedit: 3.1.0 + resolve: 1.22.10 + semver: 7.7.2 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - supports-color + + electron-publish@26.0.11: + dependencies: + '@types/fs-extra': 9.0.13 + builder-util: 26.0.11 + builder-util-runtime: 9.3.1 + chalk: 4.1.2 + form-data: 4.0.2 + fs-extra: 10.1.0 + lazy-val: 1.0.5 + mime: 2.6.0 + transitivePeerDependencies: + - supports-color + + electron-squirrel-startup@1.0.1: + dependencies: + debug: 2.6.9 + transitivePeerDependencies: + - supports-color + + electron-to-chromium@1.5.161: {} + + electron-to-chromium@1.5.283: {} + + electron-vite@3.1.0(vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3)): + dependencies: + '@babel/core': 7.27.4 + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.27.4) + cac: 6.7.14 + esbuild: 0.25.5 + magic-string: 0.30.17 + picocolors: 1.1.1 + vite: 6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3) + transitivePeerDependencies: + - supports-color + + electron-winstaller@5.4.0: + dependencies: + '@electron/asar': 3.4.1 + debug: 4.4.1 + fs-extra: 7.0.1 + lodash: 4.17.21 + temp: 0.9.4 + optionalDependencies: + '@electron/windows-sign': 1.2.2 + transitivePeerDependencies: + - supports-color + + electron@22.3.27: + dependencies: + '@electron/get': 2.0.3 + '@types/node': 16.18.126 + extract-zip: 2.0.1 + transitivePeerDependencies: + - supports-color + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + encodeurl@2.0.0: {} + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + + entities@3.0.1: {} + + entities@4.5.0: {} + + env-paths@2.2.1: {} + + err-code@2.0.3: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.1.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es6-error@4.1.1: + optional: true + + esbuild@0.25.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.5 + '@esbuild/android-arm': 0.25.5 + '@esbuild/android-arm64': 0.25.5 + '@esbuild/android-x64': 0.25.5 + '@esbuild/darwin-arm64': 0.25.5 + '@esbuild/darwin-x64': 0.25.5 + '@esbuild/freebsd-arm64': 0.25.5 + '@esbuild/freebsd-x64': 0.25.5 + '@esbuild/linux-arm': 0.25.5 + '@esbuild/linux-arm64': 0.25.5 + '@esbuild/linux-ia32': 0.25.5 + '@esbuild/linux-loong64': 0.25.5 + '@esbuild/linux-mips64el': 0.25.5 + '@esbuild/linux-ppc64': 0.25.5 + '@esbuild/linux-riscv64': 0.25.5 + '@esbuild/linux-s390x': 0.25.5 + '@esbuild/linux-x64': 0.25.5 + '@esbuild/netbsd-arm64': 0.25.5 + '@esbuild/netbsd-x64': 0.25.5 + '@esbuild/openbsd-arm64': 0.25.5 + '@esbuild/openbsd-x64': 0.25.5 + '@esbuild/sunos-x64': 0.25.5 + '@esbuild/win32-arm64': 0.25.5 + '@esbuild/win32-ia32': 0.25.5 + '@esbuild/win32-x64': 0.25.5 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: + optional: true + + esprima@4.0.1: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + etag@1.8.1: {} + + eventemitter3@5.0.1: {} + + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + + exceljs@4.4.0: + dependencies: + archiver: 5.3.2 + dayjs: 1.11.13 + fast-csv: 4.3.6 + jszip: 3.10.1 + readable-stream: 3.6.2 + saxes: 5.0.1 + tmp: 0.2.3 + unzipper: 0.10.14 + uuid: 8.3.2 + + execa@1.0.0: + dependencies: + cross-spawn: 6.0.6 + get-stream: 4.1.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 + + execa@7.2.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 4.3.1 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 3.0.7 + strip-final-newline: 3.0.0 + + expand-tilde@2.0.2: + dependencies: + homedir-polyfill: 1.0.3 + + expect-type@1.3.0: {} + + exponential-backoff@3.1.3: {} + + express-rate-limit@7.5.1(express@5.2.1): + dependencies: + express: 5.2.1 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.1 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + exsolve@1.0.8: {} + + extract-zip@2.0.1: + dependencies: + debug: 4.4.1 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + extsprintf@1.4.1: + optional: true + + fast-csv@4.3.6: + dependencies: + '@fast-csv/format': 4.3.5 + '@fast-csv/parse': 4.3.6 + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-uri@3.1.0: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fd-package-json@2.0.0: + dependencies: + walk-up-path: 4.0.0 + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fdir@6.4.5(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + + filename-reserved-regex@2.0.0: {} + + filenamify@4.3.0: + dependencies: + filename-reserved-regex: 2.0.0 + strip-outer: 1.0.1 + trim-repeated: 1.0.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@2.1.0: + dependencies: + locate-path: 2.0.0 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flora-colossus@2.0.0: + dependencies: + debug: 4.4.1 + fs-extra: 10.1.0 + transitivePeerDependencies: + - supports-color + + follow-redirects@1.15.9: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + formatly@0.3.0: + dependencies: + fd-package-json: 2.0.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-constants@1.0.0: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-extra@11.3.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-extra@11.3.2: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + fstream@1.0.12: + dependencies: + graceful-fs: 4.2.11 + inherits: 2.0.4 + mkdirp: 0.5.6 + rimraf: 2.7.1 + + function-bind@1.1.2: {} + + galactus@1.0.0: + dependencies: + debug: 4.4.1 + flora-colossus: 2.0.0 + fs-extra: 10.1.0 + transitivePeerDependencies: + - supports-color + + gar@1.0.4: + optional: true + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-folder-size@2.0.1: + dependencies: + gar: 1.0.4 + tiny-each-async: 2.0.3 + optional: true + + get-installed-path@2.1.1: + dependencies: + global-modules: 1.0.0 + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-package-info@1.0.0: + dependencies: + bluebird: 3.7.2 + debug: 2.6.9 + lodash.get: 4.4.2 + read-pkg-up: 2.0.0 + transitivePeerDependencies: + - supports-color + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stdin@9.0.0: {} + + get-stream@4.1.0: + dependencies: + pump: 3.0.2 + + get-stream@5.2.0: + dependencies: + pump: 3.0.2 + + get-stream@6.0.1: {} + + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@8.0.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.9 + once: 1.4.0 + + global-agent@3.0.0: + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.7.2 + serialize-error: 7.0.1 + optional: true + + global-modules@1.0.0: + dependencies: + global-prefix: 1.0.2 + is-windows: 1.0.2 + resolve-dir: 1.0.1 + + global-prefix@1.0.2: + dependencies: + expand-tilde: 2.0.2 + homedir-polyfill: 1.0.3 + ini: 1.3.8 + is-windows: 1.0.2 + which: 1.3.1 + + globals@11.12.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + optional: true + + goober@2.1.18(csstype@3.2.3): + dependencies: + csstype: 3.2.3 + + gopd@1.2.0: {} + + got@11.8.6: + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + + graceful-fs@4.2.11: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + optional: true + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + homedir-polyfill@1.0.3: + dependencies: + parse-passwd: 1.0.0 + + hono@4.11.7: {} + + hookable@5.5.3: {} + + hosted-git-info@2.8.9: {} + + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 + + http-cache-semantics@4.2.0: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.3 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.3 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + human-signals@4.3.1: {} + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + husky@8.0.3: {} + + i18next@25.8.13(typescript@5.9.2): + dependencies: + '@babel/runtime': 7.28.6 + optionalDependencies: + typescript: 5.9.2 + + iconv-corefoundation@1.1.7: + dependencies: + cli-truncate: 2.1.0 + node-addon-api: 1.7.2 + optional: true + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore-by-default@1.0.1: {} + + ignore@5.2.4: {} + + immediate@3.0.6: {} + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + infer-owner@1.0.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ini@3.0.1: {} + + interpret@3.1.1: {} + + ip-address@10.1.0: {} + + ipaddr.js@1.9.1: {} + + is-arrayish@0.2.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-ci@3.0.1: + dependencies: + ci-info: 3.9.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-docker@2.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@4.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-interactive@1.0.0: {} + + is-lambda@1.0.1: {} + + is-number@7.0.0: {} + + is-promise@4.0.0: {} + + is-stream@1.1.0: {} + + is-stream@3.0.0: {} + + is-unicode-supported@0.1.0: {} + + is-windows@1.0.2: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isarray@1.0.0: {} + + isbinaryfile@4.0.10: {} + + isbinaryfile@5.0.4: {} + + isbot@5.1.32: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jake@10.9.2: + dependencies: + async: 3.2.6 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + + javascript-stringify@2.1.0: {} + + jiti@2.6.1: {} + + joi@17.13.3: + dependencies: + '@hapi/hoek': 9.3.0 + '@hapi/topo': 5.1.0 + '@sideway/address': 4.1.5 + '@sideway/formula': 3.0.1 + '@sideway/pinpoint': 2.0.0 + + jose@6.1.3: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + json-stringify-safe@5.0.1: + optional: true + + json5@2.2.3: {} + + jsonc-parser@3.1.0: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + junk@3.1.0: {} + + keyboardevent-from-electron-accelerator@2.0.0: {} + + keyboardevents-areequal@0.2.2: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@4.1.5: {} + + knip@6.7.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + formatly: 0.3.0 + get-tsconfig: 4.14.0 + jiti: 2.6.1 + minimist: 1.2.8 + oxc-parser: 0.127.0 + oxc-resolver: 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + picomatch: 4.0.4 + smol-toml: 1.6.1 + strip-json-comments: 5.0.3 + tinyglobby: 0.2.16 + unbash: 3.0.0 + yaml: 2.8.3 + zod: 4.3.6 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + lazy-val@1.0.5: {} + + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + lilconfig@2.1.0: {} + + linkify-it@4.0.1: + dependencies: + uc.micro: 1.0.6 + + lint-staged@13.3.0: + dependencies: + chalk: 5.3.0 + commander: 11.0.0 + debug: 4.3.4 + execa: 7.2.0 + lilconfig: 2.1.0 + listr2: 6.6.1 + micromatch: 4.0.5 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.3.1 + transitivePeerDependencies: + - enquirer + - supports-color + + listenercount@1.0.1: {} + + listr2@5.0.8: + dependencies: + cli-truncate: 2.1.0 + colorette: 2.0.20 + log-update: 4.0.0 + p-map: 4.0.0 + rfdc: 1.4.1 + rxjs: 7.8.2 + through: 2.3.8 + wrap-ansi: 7.0.0 + + listr2@6.6.1: + dependencies: + cli-truncate: 3.1.0 + colorette: 2.0.20 + eventemitter3: 5.0.1 + log-update: 5.0.1 + rfdc: 1.4.1 + wrap-ansi: 8.1.0 + + load-json-file@2.0.0: + dependencies: + graceful-fs: 4.2.11 + parse-json: 2.2.0 + pify: 2.3.0 + strip-bom: 3.0.0 + + locate-path@2.0.0: + dependencies: + p-locate: 2.0.0 + path-exists: 3.0.0 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.defaults@4.2.0: {} + + lodash.difference@4.5.0: {} + + lodash.escaperegexp@4.1.2: {} + + lodash.flatten@4.4.0: {} + + lodash.get@4.4.2: {} + + lodash.groupby@4.6.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isequal@4.5.0: {} + + lodash.isfunction@3.0.9: {} + + lodash.isnil@4.0.0: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isundefined@3.0.1: {} + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lodash.truncate@4.4.2: {} + + lodash.union@4.6.0: {} + + lodash.uniq@4.5.0: {} + + lodash@4.17.21: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + log-update@4.0.0: + dependencies: + ansi-escapes: 4.3.2 + cli-cursor: 3.1.0 + slice-ansi: 4.0.0 + wrap-ansi: 6.2.0 + + log-update@5.0.1: + dependencies: + ansi-escapes: 5.0.0 + cli-cursor: 4.0.0 + slice-ansi: 5.0.0 + strip-ansi: 7.1.0 + wrap-ansi: 8.1.0 + + look-it-up@2.1.0: {} + + lowercase-keys@2.0.0: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + lru-cache@7.18.3: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-fetch-happen@10.2.1: + dependencies: + agentkeepalive: 4.6.0 + cacache: 16.1.3 + http-cache-semantics: 4.2.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 7.18.3 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-fetch: 2.1.2 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + promise-retry: 2.0.1 + socks-proxy-agent: 7.0.0 + ssri: 9.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + + map-age-cleaner@0.1.3: + dependencies: + p-defer: 1.0.0 + + markdown-it@13.0.1: + dependencies: + argparse: 2.0.1 + entities: 3.0.1 + linkify-it: 4.0.1 + mdurl: 1.0.1 + uc.micro: 1.0.6 + + markdownlint-cli@0.32.2: + dependencies: + commander: 9.4.1 + get-stdin: 9.0.0 + glob: 8.0.3 + ignore: 5.2.4 + js-yaml: 4.1.0 + jsonc-parser: 3.1.0 + markdownlint: 0.26.2 + markdownlint-rule-helpers: 0.17.2 + minimatch: 5.1.6 + run-con: 1.2.12 + + markdownlint-rule-helpers@0.17.2: {} + + markdownlint@0.26.2: + dependencies: + markdown-it: 13.0.1 + + matcher@3.0.0: + dependencies: + escape-string-regexp: 4.0.0 + optional: true + + math-intrinsics@1.1.0: {} + + mdurl@1.0.1: {} + + media-typer@1.1.0: {} + + mem@4.3.0: + dependencies: + map-age-cleaner: 0.1.3 + mimic-fn: 2.1.0 + p-is-promise: 2.1.0 + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + microdiff@1.5.0: {} + + micromatch@4.0.5: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@2.6.0: {} + + mimic-fn@2.1.0: {} + + mimic-fn@4.0.0: {} + + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + + minimatch@10.0.1: + dependencies: + brace-expansion: 2.0.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.0 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + minipass-collect@1.0.2: + dependencies: + minipass: 3.3.6 + + minipass-fetch@2.1.2: + dependencies: + minipass: 3.3.6 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + + minipass-flush@1.0.7: + dependencies: + minipass: 3.3.6 + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + + minipass-sized@1.0.3: + dependencies: + minipass: 3.3.6 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minipass@7.1.2: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mkdirp@1.0.4: {} + + ms@2.0.0: {} + + ms@2.1.2: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + negotiator@0.6.4: {} + + negotiator@1.0.0: {} + + nice-try@1.0.5: {} + + node-abi@3.75.0: + dependencies: + semver: 7.7.2 + + node-addon-api@1.7.2: + optional: true + + node-api-version@0.2.1: + dependencies: + semver: 7.7.2 + + node-eval@2.0.0: + dependencies: + path-is-absolute: 1.0.1 + + node-fetch@2.7.0(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + + node-releases@2.0.19: {} + + node-releases@2.0.27: {} + + nodemon@2.0.22: + dependencies: + chokidar: 3.6.0 + debug: 3.2.7(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 5.7.2 + simple-update-notifier: 1.1.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + + nopt@6.0.0: + dependencies: + abbrev: 1.1.1 + + normalize-package-data@2.5.0: + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.10 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + + normalize-path@3.0.0: {} + + normalize-url@6.1.0: {} + + npm-run-path@2.0.2: + dependencies: + path-key: 2.0.1 + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: + optional: true + + object-path@0.11.8: {} + + obug@2.1.1: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + outdent@0.8.0: {} + + oxc-parser@0.127.0: + dependencies: + '@oxc-project/types': 0.127.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.127.0 + '@oxc-parser/binding-android-arm64': 0.127.0 + '@oxc-parser/binding-darwin-arm64': 0.127.0 + '@oxc-parser/binding-darwin-x64': 0.127.0 + '@oxc-parser/binding-freebsd-x64': 0.127.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.127.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.127.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.127.0 + '@oxc-parser/binding-linux-arm64-musl': 0.127.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.127.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-musl': 0.127.0 + '@oxc-parser/binding-openharmony-arm64': 0.127.0 + '@oxc-parser/binding-wasm32-wasi': 0.127.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.127.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.127.0 + '@oxc-parser/binding-win32-x64-msvc': 0.127.0 + + oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.19.1 + '@oxc-resolver/binding-android-arm64': 11.19.1 + '@oxc-resolver/binding-darwin-arm64': 11.19.1 + '@oxc-resolver/binding-darwin-x64': 11.19.1 + '@oxc-resolver/binding-freebsd-x64': 11.19.1 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.19.1 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.19.1 + '@oxc-resolver/binding-linux-arm64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-arm64-musl': 11.19.1 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-riscv64-musl': 11.19.1 + '@oxc-resolver/binding-linux-s390x-gnu': 11.19.1 + '@oxc-resolver/binding-linux-x64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-x64-musl': 11.19.1 + '@oxc-resolver/binding-openharmony-arm64': 11.19.1 + '@oxc-resolver/binding-wasm32-wasi': 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@oxc-resolver/binding-win32-arm64-msvc': 11.19.1 + '@oxc-resolver/binding-win32-ia32-msvc': 11.19.1 + '@oxc-resolver/binding-win32-x64-msvc': 11.19.1 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + p-cancelable@2.1.1: {} + + p-defer@1.0.0: {} + + p-finally@1.0.0: {} + + p-is-promise@2.1.0: {} + + p-limit@1.3.0: + dependencies: + p-try: 1.0.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@5.0.0: + dependencies: + yocto-queue: 1.2.2 + + p-locate@2.0.0: + dependencies: + p-limit: 1.3.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + + p-try@1.0.0: {} + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + package-manager-detector@1.6.0: {} + + pako@1.0.11: {} + + parse-author@2.0.0: + dependencies: + author-regex: 1.0.0 + + parse-json@2.2.0: + dependencies: + error-ex: 1.3.2 + + parse-passwd@1.0.0: {} + + parseurl@1.3.3: {} + + path-browserify@1.0.1: {} + + path-exists@3.0.0: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@2.0.1: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-to-regexp@8.3.0: {} + + path-type@2.0.0: + dependencies: + pify: 2.3.0 + + pathe@2.0.3: {} + + pe-library@0.4.1: {} + + pend@1.2.0: {} + + perfect-debounce@1.0.0: {} + + phosphor-react@1.4.1(react@19.2.4): + dependencies: + react: 19.2.4 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + picomatch@4.0.3: {} + + picomatch@4.0.4: {} + + pidtree@0.6.0: {} + + pify@2.3.0: {} + + pkce-challenge@5.0.1: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.8 + pathe: 2.0.3 + + plist@3.1.0: + dependencies: + '@xmldom/xmldom': 0.8.10 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + + pluralize@8.0.0: {} + + postcss-discard-duplicates@7.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-discard-empty@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-merge-rules@7.0.7(postcss@8.5.6): + dependencies: + browserslist: 4.28.0 + caniuse-api: 3.0.0 + cssnano-utils: 5.0.1(postcss@8.5.6) + postcss: 8.5.6 + postcss-selector-parser: 7.1.1 + + postcss-minify-selectors@7.0.5(postcss@8.5.6): + dependencies: + cssesc: 3.0.0 + postcss: 8.5.6 + postcss-selector-parser: 7.1.1 + + postcss-nested@7.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.1 + + postcss-normalize-whitespace@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.4: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postject@1.0.0-alpha.6: + dependencies: + commander: 9.5.0 + optional: true + + prettier@2.8.8: {} + + prettier@3.2.5: {} + + prettier@3.7.3: {} + + proc-log@2.0.1: {} + + process-nextick-args@2.0.1: {} + + progress@2.0.3: {} + + promise-inflight@1.0.1: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@1.1.0: {} + + pstree.remy@1.1.8: {} + + pump@3.0.2: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + + punycode@2.3.1: {} + + qs@6.14.1: + dependencies: + side-channel: 1.1.0 + + queue-microtask@1.2.3: {} + + quick-lru@5.1.1: {} + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + rcedit@3.1.0: + dependencies: + cross-spawn-windows-exe: 1.2.0 + + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + + react-hot-toast@2.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + csstype: 3.2.3 + goober: 2.1.18(csstype@3.2.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + react-i18next@16.5.4(i18next@25.8.13(typescript@5.9.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.2): + dependencies: + '@babel/runtime': 7.28.6 + html-parse-stringify: 3.0.1 + i18next: 25.8.13(typescript@5.9.2) + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + react-dom: 19.2.4(react@19.2.4) + typescript: 5.9.2 + + react-refresh@0.17.0: {} + + react-remove-scroll-bar@2.3.8(@types/react@19.2.13)(react@19.2.4): + dependencies: + react: 19.2.4 + react-style-singleton: 2.2.3(@types/react@19.2.13)(react@19.2.4) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.13 + + react-remove-scroll@2.7.2(@types/react@19.2.13)(react@19.2.4): + dependencies: + react: 19.2.4 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.13)(react@19.2.4) + react-style-singleton: 2.2.3(@types/react@19.2.13)(react@19.2.4) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.13)(react@19.2.4) + use-sidecar: 1.1.3(@types/react@19.2.13)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + + react-style-singleton@2.2.3(@types/react@19.2.13)(react@19.2.4): + dependencies: + get-nonce: 1.0.1 + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.13 + + react@19.2.4: {} + + read-binary-file-arch@1.0.6: + dependencies: + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + read-pkg-up@2.0.0: + dependencies: + find-up: 2.1.0 + read-pkg: 2.0.0 + + read-pkg@2.0.0: + dependencies: + load-json-file: 2.0.0 + normalize-package-data: 2.5.0 + path-type: 2.0.0 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.6 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.1.2: {} + + recast@0.23.11: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + rechoir@0.8.0: + dependencies: + resolve: 1.22.10 + + regex-escape@3.4.11: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resedit@1.7.2: + dependencies: + pe-library: 0.4.1 + + resolve-alpn@1.2.1: {} + + resolve-dir@1.0.1: + dependencies: + expand-tilde: 2.0.2 + global-modules: 1.0.0 + + resolve-package@1.0.1: + dependencies: + get-installed-path: 2.1.1 + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + restore-cursor@4.0.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + retry@0.12.0: {} + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rimraf@2.6.3: + dependencies: + glob: 7.2.3 + + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + roarr@2.15.4: + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.4 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + optional: true + + rollup@4.41.1: + dependencies: + '@types/estree': 1.0.7 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.41.1 + '@rollup/rollup-android-arm64': 4.41.1 + '@rollup/rollup-darwin-arm64': 4.41.1 + '@rollup/rollup-darwin-x64': 4.41.1 + '@rollup/rollup-freebsd-arm64': 4.41.1 + '@rollup/rollup-freebsd-x64': 4.41.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.41.1 + '@rollup/rollup-linux-arm-musleabihf': 4.41.1 + '@rollup/rollup-linux-arm64-gnu': 4.41.1 + '@rollup/rollup-linux-arm64-musl': 4.41.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.41.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.41.1 + '@rollup/rollup-linux-riscv64-gnu': 4.41.1 + '@rollup/rollup-linux-riscv64-musl': 4.41.1 + '@rollup/rollup-linux-s390x-gnu': 4.41.1 + '@rollup/rollup-linux-x64-gnu': 4.41.1 + '@rollup/rollup-linux-x64-musl': 4.41.1 + '@rollup/rollup-win32-arm64-msvc': 4.41.1 + '@rollup/rollup-win32-ia32-msvc': 4.41.1 + '@rollup/rollup-win32-x64-msvc': 4.41.1 + fsevents: 2.3.3 + + rollup@4.53.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.53.3 + '@rollup/rollup-android-arm64': 4.53.3 + '@rollup/rollup-darwin-arm64': 4.53.3 + '@rollup/rollup-darwin-x64': 4.53.3 + '@rollup/rollup-freebsd-arm64': 4.53.3 + '@rollup/rollup-freebsd-x64': 4.53.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 + '@rollup/rollup-linux-arm-musleabihf': 4.53.3 + '@rollup/rollup-linux-arm64-gnu': 4.53.3 + '@rollup/rollup-linux-arm64-musl': 4.53.3 + '@rollup/rollup-linux-loong64-gnu': 4.53.3 + '@rollup/rollup-linux-ppc64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-musl': 4.53.3 + '@rollup/rollup-linux-s390x-gnu': 4.53.3 + '@rollup/rollup-linux-x64-gnu': 4.53.3 + '@rollup/rollup-linux-x64-musl': 4.53.3 + '@rollup/rollup-openharmony-arm64': 4.53.3 + '@rollup/rollup-win32-arm64-msvc': 4.53.3 + '@rollup/rollup-win32-ia32-msvc': 4.53.3 + '@rollup/rollup-win32-x64-gnu': 4.53.3 + '@rollup/rollup-win32-x64-msvc': 4.53.3 + fsevents: 2.3.3 + + router@2.2.0: + dependencies: + debug: 4.4.1 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + run-con@1.2.12: + dependencies: + deep-extend: 0.6.0 + ini: 3.0.1 + minimist: 1.2.8 + strip-json-comments: 3.1.1 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + sanitize-filename@1.6.3: + dependencies: + truncate-utf8-bytes: 1.0.2 + + sax@1.4.1: {} + + saxes@5.0.1: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.27.0: {} + + semver-compare@1.0.0: + optional: true + + semver@5.7.2: {} + + semver@6.3.1: {} + + semver@7.0.0: {} + + semver@7.7.2: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + optional: true + + seroval-plugins@1.3.3(seroval@1.3.2): + dependencies: + seroval: 1.3.2 + + seroval-plugins@1.4.0(seroval@1.4.0): + dependencies: + seroval: 1.4.0 + + seroval@1.3.2: {} + + seroval@1.4.0: {} + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setimmediate@1.0.5: {} + + setprototypeof@1.2.0: {} + + shebang-command@1.2.0: + dependencies: + shebang-regex: 1.0.0 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@1.0.0: {} + + shebang-regex@3.0.0: {} + + shell-quote@1.8.2: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-update-notifier@1.1.0: + dependencies: + semver: 7.0.0 + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.7.2 + + sisteransi@1.0.5: {} + + slice-ansi@3.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 + + smart-buffer@4.2.0: {} + + smol-toml@1.6.1: {} + + socks-proxy-agent@7.0.0: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + + solid-js@1.9.10: + dependencies: + csstype: 3.2.3 + seroval: 1.3.2 + seroval-plugins: 1.3.3(seroval@1.3.2) + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.6: {} + + spawn-command@0.0.2: {} + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.21 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.21 + + spdx-license-ids@3.0.21: {} + + sprintf-js@1.1.3: + optional: true + + ssri@9.0.1: + dependencies: + minipass: 3.3.6 + + stackback@0.0.2: {} + + stat-mode@1.0.0: {} + + statuses@2.0.2: {} + + std-env@4.1.0: {} + + string-argv@0.3.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-bom@3.0.0: {} + + strip-eof@1.0.0: {} + + strip-final-newline@3.0.0: {} + + strip-json-comments@3.1.1: {} + + strip-json-comments@5.0.3: {} + + strip-outer@1.0.1: + dependencies: + escape-string-regexp: 1.0.5 + + sudo-prompt@9.2.1: {} + + sumchecker@3.0.1: + dependencies: + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + table@6.9.0: + dependencies: + ajv: 8.17.1 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + temp-file@3.4.0: + dependencies: + async-exit-hook: 2.0.1 + fs-extra: 10.1.0 + + temp@0.9.4: + dependencies: + mkdirp: 0.5.6 + rimraf: 2.6.3 + + through@2.3.8: {} + + tiny-async-pool@1.3.0: + dependencies: + semver: 5.7.2 + + tiny-each-async@2.0.3: + optional: true + + tiny-invariant@1.3.3: {} + + tiny-warning@1.0.3: {} + + tinybench@2.9.0: {} + + tinyexec@1.1.2: {} + + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.5(picomatch@4.0.2) + picomatch: 4.0.2 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + + tmp-promise@3.0.3: + dependencies: + tmp: 0.2.3 + + tmp@0.2.3: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + touch@3.1.1: {} + + tr46@0.0.3: {} + + traverse@0.3.9: {} + + tree-kill@1.2.2: {} + + trim-repeated@1.0.0: + dependencies: + escape-string-regexp: 1.0.5 + + truncate-utf8-bytes@1.0.2: + dependencies: + utf8-byte-length: 1.0.5 + + ts-evaluator@1.2.0(typescript@5.9.2): + dependencies: + ansi-colors: 4.1.3 + crosspath: 2.0.0 + object-path: 0.11.8 + typescript: 5.9.2 + + ts-morph@27.0.2: + dependencies: + '@ts-morph/common': 0.28.1 + code-block-writer: 13.0.3 + + ts-pattern@5.9.0: {} + + tsconfck@3.1.6(typescript@5.9.2): + optionalDependencies: + typescript: 5.9.2 + + tslib@2.8.1: {} + + tsx@4.20.6: + dependencies: + esbuild: 0.25.5 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + + type-fest@0.13.1: + optional: true + + type-fest@0.21.3: {} + + type-fest@1.4.0: {} + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript@5.9.2: {} + + typescript@5.9.3: {} + + uc.micro@1.0.6: {} + + unbash@3.0.0: {} + + undefsafe@2.0.5: {} + + undici-types@6.21.0: {} + + unique-filename@2.0.1: + dependencies: + unique-slug: 3.0.0 + + unique-slug@3.0.0: + dependencies: + imurmurhash: 0.1.4 + + universalify@0.1.2: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.15.0 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + + unzip-crx-3@0.2.0: + dependencies: + jszip: 3.10.1 + mkdirp: 0.5.6 + yaku: 0.16.7 + + unzipper@0.10.14: + dependencies: + big-integer: 1.6.52 + binary: 0.3.0 + bluebird: 3.4.7 + buffer-indexof-polyfill: 1.0.2 + duplexer2: 0.1.4 + fstream: 1.0.12 + graceful-fs: 4.2.11 + listenercount: 1.0.1 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + update-browserslist-db@1.1.3(browserslist@4.25.0): + dependencies: + browserslist: 4.25.0 + escalade: 3.2.0 + picocolors: 1.1.1 + + update-browserslist-db@1.2.3(browserslist@4.28.0): + dependencies: + browserslist: 4.28.0 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@19.2.13)(react@19.2.4): + dependencies: + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.13 + + use-sidecar@1.1.3(@types/react@19.2.13)(react@19.2.4): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.13 + + use-sync-external-store@1.5.0(react@19.2.4): + dependencies: + react: 19.2.4 + + use-sync-external-store@1.6.0(react@19.2.4): + dependencies: + react: 19.2.4 + + username@5.1.0: + dependencies: + execa: 1.0.0 + mem: 4.3.0 + + utf8-byte-length@1.0.5: {} + + util-deprecate@1.0.2: {} + + uuid@8.3.2: {} + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + vary@1.1.2: {} + + verror@1.10.1: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.4.1 + optional: true + + vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3): + dependencies: + esbuild: 0.25.5 + fdir: 6.4.5(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.4 + rollup: 4.41.1 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 20.19.1 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + tsx: 4.20.6 + yaml: 2.8.3 + + vite@7.2.4(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3): + dependencies: + esbuild: 0.25.5 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.19.1 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + tsx: 4.20.6 + yaml: 2.8.3 + + vitest@4.1.6(@types/node@20.19.1)(vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.6 + '@vitest/mocker': 4.1.6(vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.6 + '@vitest/runner': 4.1.6 + '@vitest/snapshot': 4.1.6 + '@vitest/spy': 4.1.6 + '@vitest/utils': 4.1.6 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.1 + transitivePeerDependencies: + - msw + + void-elements@3.1.0: {} + + wait-on@6.0.1: + dependencies: + axios: 0.25.0 + joi: 17.13.3 + lodash: 4.17.21 + minimist: 1.2.8 + rxjs: 7.8.2 + transitivePeerDependencies: + - debug + + walk-up-path@4.0.0: {} + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + webidl-conversions@3.0.1: {} + + webpack-virtual-modules@0.6.2: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: + optional: true + + wordwrapjs@5.1.1: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrappy@1.0.2: {} + + xmlbuilder@15.1.1: {} + + xmlchars@2.2.0: {} + + y18n@5.0.8: {} + + yaku@0.16.7: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yaml@2.3.1: {} + + yaml@2.8.3: {} + + yargs-parser@20.2.9: + optional: true + + yargs-parser@21.1.1: {} + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + optional: true + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yarn-or-npm@3.0.1: + dependencies: + cross-spawn: 6.0.6 + pkg-dir: 4.2.0 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yocto-queue@0.1.0: {} + + yocto-queue@1.2.2: {} + + zip-stream@4.1.1: + dependencies: + archiver-utils: 3.0.4 + compress-commons: 4.1.2 + readable-stream: 3.6.2 + + zod-to-json-schema@3.25.1(zod@4.0.14): + dependencies: + zod: 4.0.14 + + zod@3.25.76: {} + + zod@4.0.14: {} + + zod@4.3.6: {} + + zustand@5.0.7(@types/react@19.2.13)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): + optionalDependencies: + '@types/react': 19.2.13 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml new file mode 100644 index 00000000..041eb937 --- /dev/null +++ b/frontend/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +allowBuilds: + "@biomejs/biome": true + electron-winstaller: true + electron: true + esbuild: true + +blockExoticSubdeps: false diff --git a/frontend/postcss.config.cjs b/frontend/postcss.config.cjs new file mode 100644 index 00000000..1bfc8f1d --- /dev/null +++ b/frontend/postcss.config.cjs @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + "@pandacss/dev/postcss": {}, + }, +}; diff --git a/frontend/src/main/createWindow.ts b/frontend/src/main/createWindow.ts new file mode 100644 index 00000000..379be1e7 --- /dev/null +++ b/frontend/src/main/createWindow.ts @@ -0,0 +1,73 @@ +import { join } from "node:path"; +import { is } from "@electron-toolkit/utils"; +import { BrowserWindow, shell } from "electron"; + +import { DEV_PORT, EXTERNAL_URLS, isDebug, isProduction } from "./env"; +import { resolveHTMLPath } from "./utils"; + +export let mainWindow: BrowserWindow | null; + +/** + * Configures the main `BrowserWindow` with features and handlers + * @param window Main window created on the main process + */ +function configureWindow(window: BrowserWindow | null) { + if (!window) { + throw new Error('"mainWindow" is not defined'); + } + if (isDebug) { + window.webContents.openDevTools(); + } + if (isProduction) { + window.maximize(); + } + + window.loadURL(resolveHTMLPath()); + + // window.webContents.openDevTools(); + + /** + * HANDLERS + */ + window.on("ready-to-show", () => { + window.show(); + }); + + window.webContents.setWindowOpenHandler(({ url }) => { + if (EXTERNAL_URLS.find((val) => url.includes(val))) { + shell.openExternal(url); + } + return { action: "deny" }; + }); + + // and handlers + mainWindow?.on("closed", () => { + mainWindow = null; + }); +} + +export default function createWindow() { + // Creates the browser window. + mainWindow = new BrowserWindow({ + width: 1366, + height: 768, + autoHideMenuBar: true, + webPreferences: { + preload: join(__dirname, "../preload/index.js"), + sandbox: false, + }, + }); + + // Add configuration + configureWindow(mainWindow); + + // HMR for renderer base on electron-vite cli. + // Load the remote URL for development or the local html file for production. + if (is.dev) { + mainWindow.loadURL(`http://localhost:${DEV_PORT}`); + } else { + mainWindow.loadFile(join(__dirname, "../renderer/index.html")); + } + + return mainWindow; +} diff --git a/frontend/src/main/env.ts b/frontend/src/main/env.ts new file mode 100644 index 00000000..4b7c0e76 --- /dev/null +++ b/frontend/src/main/env.ts @@ -0,0 +1,54 @@ +/// + +import { homedir } from "node:os"; +import path from "node:path"; +import { is } from "@electron-toolkit/utils"; + +// ------------------------ +// CONFIG VARIABLES +// ------------------------ + +/** + * Port for the development app + */ +export const DEV_PORT = 3000; + +/** + * Custom URI scheme name + */ +export const URI_SCHEME = "aymurai.app"; + +/** + * Exports folder + */ +export const EXPORTS_FOLDER = path.resolve(homedir(), "Documents/AymurAI"); + +/** + * URLs that must be opened in a native browser tab + */ +export const EXTERNAL_URLS = [ + "https://www.datagenero.org/", + "https://accounts.google.com/o/oauth2/v2/auth", + "https://docs.google.com/spreadsheets", + "https://www.aymurai.info", +]; + +/** + * Is the app in development mode? + */ +export const isDebug = is.dev; + +/** + * Is the app in production mode? + */ +export const isProduction = !is.dev; + +/** + * Is the app running on Windows? + */ +export const isWindows = process.platform === "win32"; + +/** + * Is the app running on macOS? + */ +export const isMac = process.platform === "darwin"; diff --git a/frontend/src/main/extensions.ts b/frontend/src/main/extensions.ts new file mode 100644 index 00000000..836b32a9 --- /dev/null +++ b/frontend/src/main/extensions.ts @@ -0,0 +1,17 @@ +import installer, { + REACT_DEVELOPER_TOOLS, + EMBER_INSPECTOR, +} from "electron-devtools-installer"; + +export const installExtensions = async (): Promise => { + const extensions = [EMBER_INSPECTOR, REACT_DEVELOPER_TOOLS]; + + return installer(extensions, { + forceDownload: true, + loadExtensionOptions: { + allowFileAccess: true, + }, + }) + .then(console.log) + .catch(console.error); +}; diff --git a/frontend/src/main/index.ts b/frontend/src/main/index.ts new file mode 100644 index 00000000..1f133afc --- /dev/null +++ b/frontend/src/main/index.ts @@ -0,0 +1,54 @@ +import { electronApp, optimizer } from "@electron-toolkit/utils"; +import { BrowserWindow, app, ipcMain } from "electron"; + +import createWindow from "./createWindow"; +import { installExtensions } from "./extensions"; +import { excel, feedback, subprocess, taskbar } from "./utils"; + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(async () => { + // Set app user model id for windows + electronApp.setAppUserModelId("com.electron"); + + // Default open or close DevTools by F12 in development + // and ignore CommandOrControl + R in production. + // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils + app.on("browser-window-created", (_, window) => { + optimizer.watchWindowShortcuts(window); + }); + + ipcMain.handle("EXPORT_FEEDBACK", (_, fileName: string, data: object) => + feedback.export(fileName, data), + ); + ipcMain.handle("EXCEL_READ", excel.read); + ipcMain.handle("EXCEL_WRITE", (_, buffer: ArrayBuffer) => + excel.write(buffer), + ); + ipcMain.handle("EXCEL_OPEN", excel.open); + + // TASKBAR + ipcMain.handle("TASKBAR_NOTIFY", taskbar.notify); + + ipcMain.handle("RUN_BATCH", subprocess.run); + + await installExtensions(); + createWindow(); + + app.on("activate", () => { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); +}); + +// Quit when all windows are closed, except on macOS. There, it's common +// for applications and their menu bar to stay active until the user quits +// explicitly with Cmd + Q. +app.on("window-all-closed", async () => { + subprocess.stop(); + if (process.platform !== "darwin") { + app.quit(); + } +}); diff --git a/frontend/src/main/utils/__tests__/excel.test.ts b/frontend/src/main/utils/__tests__/excel.test.ts new file mode 100644 index 00000000..235565e7 --- /dev/null +++ b/frontend/src/main/utils/__tests__/excel.test.ts @@ -0,0 +1,52 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { Workbook } from "exceljs"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("electron", () => ({ + shell: { + openPath: vi.fn(), + }, +})); + +vi.mock("@electron-toolkit/utils", () => ({ + is: { + dev: true, + }, +})); + +describe("main excel filesystem bridge", () => { + let tempHome: string; + + beforeEach(async () => { + tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "aymurai-excel-")); + vi.spyOn(os, "homedir").mockReturnValue(tempHome); + vi.resetModules(); + }); + + afterEach(async () => { + vi.restoreAllMocks(); + await fs.rm(tempHome, { recursive: true, force: true }); + }); + + it("returns the exact file bytes when reading the dataset workbook", async () => { + const { default: excel } = await import("../excel"); + const workbook = new Workbook(); + workbook.addWorksheet("set_de_datos").addRow(["ok"]); + + const original = await workbook.xlsx.writeBuffer(); + await excel.write(original); + + const loaded = await excel.read(); + expect(loaded.byteLength).toBe(original.byteLength); + + const reloaded = new Workbook(); + const loadedBuffer = Buffer.from(new Uint8Array(loaded as ArrayBuffer)); + const loadWorkbook = reloaded.xlsx.load.bind(reloaded.xlsx) as ( + data: unknown, + ) => Promise; + await expect(loadWorkbook(loadedBuffer)).resolves.toBeDefined(); + expect(reloaded.worksheets[0].getCell("A1").value).toBe("ok"); + }); +}); diff --git a/frontend/src/main/utils/excel.ts b/frontend/src/main/utils/excel.ts new file mode 100644 index 00000000..5548796b --- /dev/null +++ b/frontend/src/main/utils/excel.ts @@ -0,0 +1,44 @@ +import fs from "node:fs/promises"; +import { shell } from "electron"; + +import { EXPORTS_FOLDER } from "../env"; +import filesystem from "./filesystem"; + +const FILENAME = "set_de_datos.xlsx"; +const PATH = `${EXPORTS_FOLDER}/${FILENAME}`; + +/** + * Reads the raw data from the data_set excel file + * @returns The `Promise` read from the file + */ +function read() { + return fs.readFile(PATH).then(({ buffer }) => buffer); +} + +/** + * Writes a .xlsx file to the filesystem + * @param buffer Data buffer to write. This is the .xlsx file + */ +async function write(arrBuffer: ArrayBuffer) { + // Create directory if necessary + if (!(await filesystem.exists(EXPORTS_FOLDER))) { + await fs.mkdir(EXPORTS_FOLDER, { recursive: true }); + } + + await fs.writeFile(PATH, Buffer.from(arrBuffer)); +} + +/** + * Opens the previously created .xlsx using the default app + * @returns Message if the opening was succesfull or not + */ +function open() { + return shell.openPath(PATH); +} + +const excel = { + read, + write, + open, +}; +export default excel; diff --git a/frontend/src/main/utils/feedback.ts b/frontend/src/main/utils/feedback.ts new file mode 100644 index 00000000..0224e3f5 --- /dev/null +++ b/frontend/src/main/utils/feedback.ts @@ -0,0 +1,61 @@ +import { promises as fs } from "node:fs"; + +import { EXPORTS_FOLDER } from "../env"; +import filesystem from "./filesystem"; + +const FEEDBACK_FOLDER = `${EXPORTS_FOLDER}/feedback`; + +/** + * Gets the current date and time in the format `YYYY-MM-DD-HH_MM_SS` + */ +function getDate() { + const now = new Date(Date.now()); + const [date, time] = now.toISOString().split("T"); + + const fileDate = `${date}-${ + time // Time in format xx:yy:zz.zzz + .split(".")[0] // We are left with xx:yy:zz + .replace(/:/g, "_") // Transform it into xx_yy_zz + }`; + + return fileDate; +} + +function formatName(fileName: string) { + // Remove extension and spaces from the name + const formattedName = fileName + .replace(/\.docx/g, "") // Remove extension + .replace(/\s/g, "_"); // Remove whitespace + + return `aymurai--${formattedName}`; +} + +async function mkdir() { + await fs.mkdir(FEEDBACK_FOLDER, { recursive: true }); +} + +/** + * Parses a validation object into a JSON that is written into a feedback file + * @param fileName Name of the file + * @param content Validation object + */ +async function exportFeedback(fileName: string, content: object) { + const date = getDate(); + const name = formatName(fileName); + const json = JSON.stringify(content); + + // Create directory if necessary + if (!(await filesystem.exists(FEEDBACK_FOLDER))) { + await mkdir(); + } + + // Create file + await fs.writeFile(`${FEEDBACK_FOLDER}/${name}--${date}.json`, json, { + flag: "w", + }); +} + +const feedback = { + export: exportFeedback, +}; +export default feedback; diff --git a/frontend/src/main/utils/filesystem.ts b/frontend/src/main/utils/filesystem.ts new file mode 100644 index 00000000..bba048e0 --- /dev/null +++ b/frontend/src/main/utils/filesystem.ts @@ -0,0 +1,20 @@ +import fs from "node:fs/promises"; + +/** + * Check for the existence of a specific director or file + * @param path Path to check + * @returns `true` if the directory/file exists, `false` otherwise + */ +async function exists(path: string) { + try { + await fs.access(path, fs.constants.F_OK); + return true; + } catch { + return false; + } +} + +const filesystem = { + exists, +}; +export default filesystem; diff --git a/frontend/src/main/utils/index.ts b/frontend/src/main/utils/index.ts new file mode 100644 index 00000000..7c4176e3 --- /dev/null +++ b/frontend/src/main/utils/index.ts @@ -0,0 +1,7 @@ +import excel from "./excel"; +import feedback from "./feedback"; +import resolveHTMLPath from "./resolveHTMLPath"; +import taskbar from "./taskbar"; +export { excel, feedback, resolveHTMLPath, taskbar }; + +export * as subprocess from "./subprocess"; diff --git a/frontend/src/main/utils/resolveHTMLPath.ts b/frontend/src/main/utils/resolveHTMLPath.ts new file mode 100644 index 00000000..e7f0e539 --- /dev/null +++ b/frontend/src/main/utils/resolveHTMLPath.ts @@ -0,0 +1,20 @@ +import path from "node:path"; +import { URL } from "node:url"; + +import { DEV_PORT, isDebug } from "../env"; + +/** + * Retrieves the HMTL path to the requested file + * @param fileName Name of the file, default to `index.html` + * @returns An HTML path to the file, wether on the file system or the web + */ +export default function resolveHTMLPath(fileName = "index.html") { + if (isDebug) { + const url = new URL(`http://localhost:${DEV_PORT}`); + + return url.href; + } + + const pathToFile = path.resolve(__dirname, "../app", fileName); + return `file://${pathToFile}`; +} diff --git a/frontend/src/main/utils/subprocess.ts b/frontend/src/main/utils/subprocess.ts new file mode 100644 index 00000000..0dd06696 --- /dev/null +++ b/frontend/src/main/utils/subprocess.ts @@ -0,0 +1,31 @@ +import { exec, spawn } from "node:child_process"; +import path from "node:path"; +import { app } from "electron"; + +export const run = () => { + const batFilePath = path.join(app.getPath("exe"), "../run_server.bat"); + const child = spawn(batFilePath); + + return new Promise((ok, no) => { + if (!child) return no(new Error("Child process not found")); + + child.on("spawn", () => { + ok(true); + }); + child.on("error", (err) => { + no(err); + }); + }); +}; + +export const stop = (): Promise => { + return new Promise((ok, no) => { + exec( + 'powershell "& Get-Process -Name python | ? { $_.Path -eq \\"$env:USERPROFILE\\miniconda3\\envs\\aymurai-backend\\python.exe\\"} | Stop-Process"', + (err) => { + if (err) return no(err); + ok(true); + }, + ); + }); +}; diff --git a/frontend/src/main/utils/taskbar.ts b/frontend/src/main/utils/taskbar.ts new file mode 100644 index 00000000..dca71e34 --- /dev/null +++ b/frontend/src/main/utils/taskbar.ts @@ -0,0 +1,27 @@ +import { app } from "electron"; +import { mainWindow } from "../createWindow"; +import { isMac, isWindows } from "../env"; + +/** + * Notify the user through the taskbar + */ +const notify = () => { + if (isWindows) { + if (!mainWindow) return; + + // Flash taskbar icon (windows) + mainWindow.flashFrame(true); + + mainWindow.once("focus", () => mainWindow?.flashFrame(false)); + } else if (isMac) { + if (!app || !app.dock) return; + + // Bounce dock icon (macOS) + app.dock.bounce("informational"); + } +}; + +const taskbar = { + notify, +}; +export default taskbar; diff --git a/frontend/src/preload/index.d.ts b/frontend/src/preload/index.d.ts new file mode 100644 index 00000000..5d8869bd --- /dev/null +++ b/frontend/src/preload/index.d.ts @@ -0,0 +1,22 @@ +declare global { + interface Window { + filesystem?: { + feedback: { + export: (fileName: string, data: object) => Promise; + }; + excel: { + read: () => Promise; + write: (arrBuffer: ArrayBuffer) => Promise; + open: () => Promise; + }; + }; + taskbar?: { + notify: () => Promise; + }; + electronAPI?: { + runBatch: () => Promise; + }; + } +} + +export {}; diff --git a/frontend/src/preload/index.ts b/frontend/src/preload/index.ts new file mode 100644 index 00000000..a1a0ebbc --- /dev/null +++ b/frontend/src/preload/index.ts @@ -0,0 +1,39 @@ +import { electronAPI } from "@electron-toolkit/preload"; +import { contextBridge, ipcRenderer } from "electron"; + +// Custom APIs for renderer +const api = {}; + +// Use `contextBridge` APIs to expose Electron APIs to +// renderer only if context isolation is enabled, otherwise +// just add to the DOM global. +if (process.contextIsolated) { + try { + contextBridge.exposeInMainWorld("filesystem", { + feedback: { + export: (fileName: string, object: object) => + ipcRenderer.invoke("EXPORT_FEEDBACK", fileName, object), + }, + excel: { + read: () => ipcRenderer.invoke("EXCEL_READ"), + write: (buffer: Buffer) => ipcRenderer.invoke("EXCEL_WRITE", buffer), + open: () => ipcRenderer.invoke("EXCEL_OPEN"), + }, + }); + + contextBridge.exposeInMainWorld("taskbar", { + notify: () => ipcRenderer.invoke("TASKBAR_NOTIFY"), + }); + + contextBridge.exposeInMainWorld("electronAPI", { + runBatch: (result: unknown) => ipcRenderer.invoke("RUN_BATCH", result), + }); + } catch (error) { + console.error(error); + } +} else { + // @ts-ignore (define in dts) + window.electron = electronAPI; + // @ts-ignore (define in dts) + window.api = api; +} diff --git a/frontend/src/renderer/index.html b/frontend/src/renderer/index.html new file mode 100644 index 00000000..93cd96fa --- /dev/null +++ b/frontend/src/renderer/index.html @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + AymurAI + + + +
+ + + + \ No newline at end of file diff --git a/frontend/src/renderer/public/audio/notification.mp3 b/frontend/src/renderer/public/audio/notification.mp3 new file mode 100644 index 00000000..6b4b277f Binary files /dev/null and b/frontend/src/renderer/public/audio/notification.mp3 differ diff --git a/frontend/src/renderer/public/brand/aymurai-hor-darkpurple.svg b/frontend/src/renderer/public/brand/aymurai-hor-darkpurple.svg new file mode 100644 index 00000000..efde8df1 --- /dev/null +++ b/frontend/src/renderer/public/brand/aymurai-hor-darkpurple.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/renderer/public/brand/aymurai-iso-darkpurple.svg b/frontend/src/renderer/public/brand/aymurai-iso-darkpurple.svg new file mode 100644 index 00000000..9fa89d34 --- /dev/null +++ b/frontend/src/renderer/public/brand/aymurai-iso-darkpurple.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/renderer/public/brand/aymurai-iso-purple.svg b/frontend/src/renderer/public/brand/aymurai-iso-purple.svg new file mode 100644 index 00000000..35114f7c --- /dev/null +++ b/frontend/src/renderer/public/brand/aymurai-iso-purple.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/renderer/public/brand/aymurai-vert-darkpurple.svg b/frontend/src/renderer/public/brand/aymurai-vert-darkpurple.svg new file mode 100644 index 00000000..452bbe89 --- /dev/null +++ b/frontend/src/renderer/public/brand/aymurai-vert-darkpurple.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/renderer/public/brand/aymurai-vert-purple.png b/frontend/src/renderer/public/brand/aymurai-vert-purple.png new file mode 100644 index 00000000..fa78e70d Binary files /dev/null and b/frontend/src/renderer/public/brand/aymurai-vert-purple.png differ diff --git a/frontend/src/renderer/public/brand/datagenero.svg b/frontend/src/renderer/public/brand/datagenero.svg new file mode 100644 index 00000000..a07225aa --- /dev/null +++ b/frontend/src/renderer/public/brand/datagenero.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/frontend/src/renderer/public/brand/iso-white.png b/frontend/src/renderer/public/brand/iso-white.png new file mode 100644 index 00000000..5c1359f1 Binary files /dev/null and b/frontend/src/renderer/public/brand/iso-white.png differ diff --git a/frontend/src/renderer/public/brand/logo.jpg b/frontend/src/renderer/public/brand/logo.jpg new file mode 100644 index 00000000..a8e38723 Binary files /dev/null and b/frontend/src/renderer/public/brand/logo.jpg differ diff --git a/frontend/src/renderer/public/button-icons/add-all.svg b/frontend/src/renderer/public/button-icons/add-all.svg new file mode 100644 index 00000000..d601b7f1 --- /dev/null +++ b/frontend/src/renderer/public/button-icons/add-all.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/renderer/public/button-icons/add-one.svg b/frontend/src/renderer/public/button-icons/add-one.svg new file mode 100644 index 00000000..3ecb521c --- /dev/null +++ b/frontend/src/renderer/public/button-icons/add-one.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/renderer/public/button-icons/delete-all.svg b/frontend/src/renderer/public/button-icons/delete-all.svg new file mode 100644 index 00000000..9a3f48bf --- /dev/null +++ b/frontend/src/renderer/public/button-icons/delete-all.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/renderer/public/button-icons/delete-one.svg b/frontend/src/renderer/public/button-icons/delete-one.svg new file mode 100644 index 00000000..aebeacbe --- /dev/null +++ b/frontend/src/renderer/public/button-icons/delete-one.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/renderer/public/button-icons/replace-all.svg b/frontend/src/renderer/public/button-icons/replace-all.svg new file mode 100644 index 00000000..109ec00d --- /dev/null +++ b/frontend/src/renderer/public/button-icons/replace-all.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/renderer/public/button-icons/replace-one.svg b/frontend/src/renderer/public/button-icons/replace-one.svg new file mode 100644 index 00000000..409bb51b --- /dev/null +++ b/frontend/src/renderer/public/button-icons/replace-one.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/renderer/public/favicon.ico b/frontend/src/renderer/public/favicon.ico new file mode 100644 index 00000000..7e589465 Binary files /dev/null and b/frontend/src/renderer/public/favicon.ico differ diff --git a/frontend/src/renderer/public/manifest.json b/frontend/src/renderer/public/manifest.json new file mode 100644 index 00000000..3536cc5d --- /dev/null +++ b/frontend/src/renderer/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "Electron App", + "name": "Electron Desktop App", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/frontend/src/renderer/public/onboarding-steps/step1.png b/frontend/src/renderer/public/onboarding-steps/step1.png new file mode 100644 index 00000000..1e560fe4 Binary files /dev/null and b/frontend/src/renderer/public/onboarding-steps/step1.png differ diff --git a/frontend/src/renderer/public/onboarding-steps/step2.png b/frontend/src/renderer/public/onboarding-steps/step2.png new file mode 100644 index 00000000..51454b27 Binary files /dev/null and b/frontend/src/renderer/public/onboarding-steps/step2.png differ diff --git a/frontend/src/renderer/public/onboarding-steps/step3.png b/frontend/src/renderer/public/onboarding-steps/step3.png new file mode 100644 index 00000000..9afdd3f7 Binary files /dev/null and b/frontend/src/renderer/public/onboarding-steps/step3.png differ diff --git a/frontend/src/renderer/public/onboarding-steps/step4.png b/frontend/src/renderer/public/onboarding-steps/step4.png new file mode 100644 index 00000000..01abcb98 Binary files /dev/null and b/frontend/src/renderer/public/onboarding-steps/step4.png differ diff --git a/frontend/src/renderer/public/robots.txt b/frontend/src/renderer/public/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/frontend/src/renderer/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/frontend/src/renderer/src/app.tsx b/frontend/src/renderer/src/app.tsx new file mode 100644 index 00000000..da40d36e --- /dev/null +++ b/frontend/src/renderer/src/app.tsx @@ -0,0 +1,49 @@ +import { + RouterProvider, + createMemoryHistory, + createRouter, +} from "@tanstack/react-router"; +import { Toaster } from "react-hot-toast"; + +import { ThemeProvider } from "@/components"; +import FileProvider from "@/context/File"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import * as TanstackReactQuery from "@/features/ReactQueryProvider"; + +// Import the generated route tree +import { routeTree } from "./routeTree.gen"; + +const TanStackQueryProviderContext = TanstackReactQuery.getContext(); + +const history = + import.meta.env.VITE_APP_MODE === "electron" + ? createMemoryHistory({ initialEntries: ["/"] }) + : undefined; +const router = createRouter({ + routeTree, + history, + context: { ...TanStackQueryProviderContext }, + defaultViewTransition: true, +}); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +export default function App() { + return ( + + {/* Stitches global styles */} + + + + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/anonymizer/anonymizer-label-select.tsx b/frontend/src/renderer/src/components/anonymizer/anonymizer-label-select.tsx new file mode 100644 index 00000000..c4d5e774 --- /dev/null +++ b/frontend/src/renderer/src/components/anonymizer/anonymizer-label-select.tsx @@ -0,0 +1,370 @@ +import * as RadixSelect from "@radix-ui/react-select"; +import { CaretDown, Check, MagnifyingGlass } from "phosphor-react"; +import { useEffect, useId, useMemo, useRef, useState } from "react"; + +import type { SelectOption } from "@/components/ui/select"; +import { + ANONYMIZER_CATEGORY_NAMES, + getAnonymizerLabelsForCategory, +} from "@/constants/anonymizer-categories"; +import { css, sva } from "@/styled/css"; +import { styled } from "@/styled/jsx"; +import { stack } from "@/styled/patterns"; +import { normalizeEntityText } from "@/utils/anonymizer/entity-similarity"; + +interface AnonymizerLabelSelectProps { + options: SelectOption[]; + value?: string; + onChange?: (value: SelectOption) => void; + onOpenChange?: (open: boolean) => void; + placeholder?: string; + disabled?: boolean; + size?: "md" | "sm"; + currentCategory?: string; +} + +interface LabelSection { + category: string; + labels: SelectOption[]; +} + +const select = sva({ + slots: [ + "container", + "trigger", + "value", + "caret", + "content", + "search", + "searchInput", + "viewport", + "groupLabel", + "item", + "itemIndicator", + "empty", + ], + base: { + container: { ...stack.raw({ gap: "1" }), width: "full" }, + trigger: { + display: "flex", + alignItems: "center", + gap: "2", + width: "full", + bg: "white", + border: "primary", + rounded: "sm", + cursor: "pointer", + textAlign: "left", + appearance: "none", + textStyle: "label.md.default", + color: "text.default", + + "&[data-state='open']": { + boxShadow: "[0px 2px 2px rgba(0, 0, 0, 0.16)]", + borderColor: "[#110041]", + }, + "&:focus-visible": { + outlineColor: "brand.primary", + outlineWidth: "0.5", + outlineStyle: "solid", + outlineOffset: "0.5", + }, + "&[data-disabled]": { + bg: "bg.primary", + cursor: "not-allowed", + color: "text.lighter", + }, + "&[data-placeholder]": { + color: "text.lighter", + }, + }, + value: { + flex: "[1]", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }, + caret: { + flexShrink: "0", + color: "text.default", + transition: "[transform 0.15s ease]", + + "[data-state='open'] > &": { + transform: "[rotate(180deg)]", + }, + }, + content: { + bg: "white", + rounded: "sm", + boxShadow: "[0px 8px 16px rgba(0, 0, 0, 0.08)]", + border: "secondary", + overflow: "hidden", + zIndex: "50", + minWidth: "[var(--radix-select-trigger-width)]", + }, + search: { + display: "flex", + alignItems: "center", + gap: "2", + px: "3", + py: "2", + borderBottom: "[1px solid #E5E1DD]", + color: "text.lighter", + }, + searchInput: { + width: "full", + minW: "0", + outline: "none", + color: "text.default", + textStyle: "label.md.default", + "&::placeholder": { + color: "text.lighter", + }, + }, + viewport: { + maxHeight: "[360px]", + overflowY: "auto", + py: "1", + }, + groupLabel: { + px: "3", + pt: "3", + pb: "1", + textStyle: "label.sm.default", + color: "text.lighter", + textTransform: "uppercase", + }, + item: { + display: "flex", + alignItems: "center", + gap: "2", + textStyle: "label.md.default", + color: "text.default", + cursor: "pointer", + outline: "none", + userSelect: "none", + + "&[data-highlighted]": { + bg: "bg.primary-alternative", + }, + "&[data-state='checked']": { + bg: "bg.primary-alternative", + }, + }, + itemIndicator: { + color: "brand.primary", + display: "flex", + alignItems: "center", + flexShrink: "0", + }, + empty: { + px: "3", + py: "4", + textStyle: "label.md.default", + color: "text.lighter", + }, + }, + variants: { + size: { + md: { + trigger: { px: "3", py: "3" }, + item: { px: "3", py: "3" }, + }, + sm: { + trigger: { px: "3", py: "1" }, + item: { px: "3", py: "3" }, + }, + }, + }, + defaultVariants: { + size: "md", + }, +}); + +const technicalId = css({ + color: "text.lighter", + textStyle: "label.sm.default", + whiteSpace: "nowrap", +}); + +function labelMatches(option: SelectOption, query: string) { + const normalizedQuery = normalizeEntityText(query); + if (!normalizedQuery) return true; + + const haystacks = [option.id, option.text, option.shortText ?? ""] + .map((value) => normalizeEntityText(value)) + .filter(Boolean); + + if (haystacks.some((value) => value.includes(normalizedQuery))) return true; + + const labelTokens = haystacks.flatMap((value) => value.split(" ")); + return normalizedQuery + .split(" ") + .every((queryToken) => + labelTokens.some((labelToken) => labelToken.includes(queryToken)), + ); +} + +function buildSections( + options: SelectOption[], + query: string, + currentCategory?: string, +): LabelSection[] { + const optionById = new Map(options.map((option) => [option.id, option])); + const seen = new Set(); + const categories = currentCategory + ? [ + currentCategory, + ...ANONYMIZER_CATEGORY_NAMES.filter( + (category) => category !== currentCategory, + ), + ] + : ANONYMIZER_CATEGORY_NAMES; + + return categories + .map((category) => { + const labels = getAnonymizerLabelsForCategory(category) + .map((label) => optionById.get(label.id)) + .filter((label): label is SelectOption => !!label) + .filter((label) => { + if (seen.has(label.id)) return false; + return labelMatches(label, query); + }); + + labels.forEach((label) => seen.add(label.id)); + + return { category, labels }; + }) + .filter((section) => section.labels.length > 0); +} + +export default function AnonymizerLabelSelect({ + options, + value, + onChange, + onOpenChange, + placeholder = "", + disabled = false, + size = "md", + currentCategory, +}: AnonymizerLabelSelectProps) { + const [query, setQuery] = useState(""); + const [open, setOpen] = useState(false); + const triggerId = useId(); + const searchInputRef = useRef(null); + const classes = select({ size }); + + const selectedOption = options.find((option) => option.id === value); + const sections = useMemo( + () => buildSections(options, query, currentCategory), + [currentCategory, options, query], + ); + + useEffect(() => { + if (!open) return; + const timer = window.setTimeout( + () => searchInputRef.current?.focus({ preventScroll: true }), + 0, + ); + return () => window.clearTimeout(timer); + }, [open]); + + const handleOpenChange = (nextOpen: boolean) => { + setOpen(nextOpen); + onOpenChange?.(nextOpen); + if (!nextOpen) setQuery(""); + }; + + const handleChange = (id: string) => { + const option = options.find((item) => item.id === id); + if (option) onChange?.(option); + }; + + return ( +
+ + + {/* biome-ignore lint/a11y/useSemanticElements lint/a11y/useAriaPropsForRole: Radix merges role, aria-expanded, aria-controls, and tabIndex onto this div at runtime via asChild */} +
+ + {selectedOption?.shortText ?? selectedOption?.text ?? placeholder} + + + +
+
+ + + +
+ + { + setQuery(event.target.value); + window.requestAnimationFrame(() => + searchInputRef.current?.focus({ preventScroll: true }), + ); + }} + onPointerDown={(event) => event.stopPropagation()} + onKeyDownCapture={(event) => { + if (event.key !== "Escape") event.stopPropagation(); + }} + onKeyDown={(event) => { + if (event.key !== "Escape") event.stopPropagation(); + }} + /> +
+ + {sections.length === 0 ? ( +
Sin resultados
+ ) : ( + sections.map((section) => ( + + + {section.category} + + {section.labels.map((option) => ( + + + + + + {option.text} + + {option.id} + + ))} + + )) + )} +
+
+
+
+
+ ); +} diff --git a/frontend/src/renderer/src/components/anonymizer/label-manager/config-tab.tsx b/frontend/src/renderer/src/components/anonymizer/label-manager/config-tab.tsx new file mode 100644 index 00000000..16c956d9 --- /dev/null +++ b/frontend/src/renderer/src/components/anonymizer/label-manager/config-tab.tsx @@ -0,0 +1,232 @@ +import { HStack, Stack, styled } from "@/styled/jsx"; +import type { AnonymizerLabels } from "@/types/aymurai"; +import { useState } from "react"; + +import Button from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import Input from "@/components/ui/input"; +import BaseSwitch from "@/components/ui/switch"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { + ANONYMIZER_CATEGORY_NAMES, + getAnonymizerLabelsForCategory, +} from "@/constants/anonymizer-categories"; +import { EXCLUDED_TAGS } from "@/constants/excluded-tags"; +import { + useExcludedTagsConfig, + useExcludedTagsConfigActions, +} from "@/store/useLocal"; +import { css } from "@/styled/css"; +import { Trash } from "phosphor-react"; +import { Label } from "./label"; +import LabelManagerSection from "./section"; + +const iconButton = css({ + cursor: "pointer", + color: "text.lighter", + bg: "transparent", + border: "[1px solid #BCBAB8]", + rounded: "sm", + display: "flex", + alignItems: "center", + justifyContent: "center", + p: "1.5", + flexShrink: "0", + "&:hover": { + color: "white", + bg: "action.hover", + borderColor: "action.hover", + }, +}); + +const tooltipContent = css({ + bg: "action.hover", + color: "white", + px: "1.5", + py: "0.5", + rounded: "sm", + fontSize: "[12px]", + boxShadow: "[none]", +}); + +interface ToggleProps { + name: string; + value: boolean; + onToggle: (value: boolean) => void; +} +function Switch({ name, value, onToggle }: ToggleProps) { + return ( + + + + + {name} + + + + + + Incluir o excluir esta categoría de la anonimización + + + ); +} + +interface LabelConfigTabProps { + expandedSections: Record; + onSectionOpenChange: (section: string, open: boolean) => void; +} + +const EXCLUDED_TERMS_SECTION = "Términos excluidos"; + +export default function LabelConfigTab({ + expandedSections, + onSectionOpenChange, +}: LabelConfigTabProps) { + const { tags: storedTags, words: storedWords } = useExcludedTagsConfig(); + const { setTags, setWords } = useExcludedTagsConfigActions(); + + const [toggles, setToggles] = useState>( + storedTags ?? EXCLUDED_TAGS, + ); + const [excludedWords, setExcludedWords] = useState(storedWords); + const [inputValue, setInputValue] = useState(""); + const [clearDialogOpen, setClearDialogOpen] = useState(false); + + function handleToggle(id: string, value: boolean) { + const next = { ...toggles, [id]: value }; + setToggles(next); + setTags(next as Record); + } + + function handleAddWord() { + const trimmed = inputValue.trim(); + if (!trimmed || excludedWords.includes(trimmed)) return; + const next = [...excludedWords, trimmed]; + setExcludedWords(next); + setWords(next); + setInputValue(""); + } + + function handleRemoveWord(word: string) { + const next = excludedWords.filter((w) => w !== word); + setExcludedWords(next); + setWords(next); + } + + function handleClearAllWords() { + setExcludedWords([]); + setWords([]); + } + + return ( + + + {ANONYMIZER_CATEGORY_NAMES.map((category, index) => ( + + {index > 0 && } + onSectionOpenChange(category, open)} + > + + {getAnonymizerLabelsForCategory(category).map((label) => ( + handleToggle(label.id, value)} + /> + ))} + + + + ))} + + + onSectionOpenChange(EXCLUDED_TERMS_SECTION, open) + } + headerAction={ + excludedWords.length > 0 ? ( + + ) : undefined + } + > + + + setInputValue(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleAddWord()} + /> + + + {excludedWords.length > 0 && ( + + {excludedWords.map((word) => ( + + ))} + + )} + + + + + + Borrar términos excluidos + + + Esta acción no se puede deshacer. ¿Deseas continuar? + + + + + + + + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/anonymizer/label-manager/entity-tab.tsx b/frontend/src/renderer/src/components/anonymizer/label-manager/entity-tab.tsx new file mode 100644 index 00000000..91e6c096 --- /dev/null +++ b/frontend/src/renderer/src/components/anonymizer/label-manager/entity-tab.tsx @@ -0,0 +1,959 @@ +import AnonymizerLabelSelect from "@/components/anonymizer/anonymizer-label-select"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { + ANONYMIZER_CATEGORY_NAMES, + getAnonymizerCategoryForLabel, + getLabelsPrioritizingCategory, +} from "@/constants/anonymizer-categories"; +import { useFileDispatch, useFiles } from "@/hooks"; +import { useEntityGroups } from "@/hooks/useEntityGroups"; +import { + mergeGroups, + moveMentionToGroup, + removePredictionValueByCanonicalId, + removePredictionsByCanonicalId, + updatePredictionsByCanonicalId, +} from "@/reducers/file/actions"; +import { useHoverState } from "@/store/useHoverState"; +import { css } from "@/styled/css"; +import { HStack, Stack, styled } from "@/styled/jsx"; +import type { AllLabels, AllLabelsWithSufix } from "@/types/aymurai"; +import { + DndContext, + type DragEndEvent, + type DragOverEvent, + DragOverlay, + type DragStartEvent, + KeyboardSensor, + PointerSensor, + closestCenter, + useDraggable, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { + SortableContext, + arrayMove, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { + ArrowsLeftRight, + DotsSixVertical, + PlusCircle, + Trash, + Warning, + XCircle, +} from "phosphor-react"; +import { useEffect, useMemo, useState } from "react"; + +import type { EntityGroup } from "@/hooks/useEntityGroups"; +import { + useExcludedTagsConfig, + useGroupOrder, + useGroupOrderActions, +} from "@/store/useLocal"; +import { normalizeEntityText } from "@/utils/anonymizer/entity-similarity"; +import { filterActivePredictions } from "@/utils/anonymizer/predictions"; +import MergeDialog from "./merge-dialog"; +import RemoveDialog from "./remove-dialog"; +import LabelManagerSection from "./section"; +import SortableGroup from "./sortable-group"; + +function normalizeText(t: string) { + return normalizeEntityText(t); +} + +const GROUP_PREFIX = "group:"; +const TEXT_PREFIX = "text:"; + +function groupDndId(id: string) { + return `${GROUP_PREFIX}${id}`; +} +function textDndId(canonicalId: string, normText: string) { + return `${TEXT_PREFIX}${canonicalId}:::${normText}`; +} +function parseGroupDndId(id: string): string | null { + return id.startsWith(GROUP_PREFIX) ? id.slice(GROUP_PREFIX.length) : null; +} +function parseTextDndId( + id: string, +): { canonicalId: string; text: string } | null { + if (!id.startsWith(TEXT_PREFIX)) return null; + const rest = id.slice(TEXT_PREFIX.length); + const sep = rest.indexOf(":::"); + if (sep === -1) return null; + return { canonicalId: rest.slice(0, sep), text: rest.slice(sep + 3) }; +} + +const iconButton = css({ + cursor: "pointer", + color: "text.lighter", + bg: "transparent", + border: "[1px solid #BCBAB8]", + rounded: "sm", + display: "flex", + alignItems: "center", + justifyContent: "center", + p: "1.5", + flexShrink: "0", + "&:hover": { + color: "white", + bg: "action.hover", + borderColor: "action.hover", + }, +}); + +const suffixBadge = css({ + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + px: "1.5", + py: "0.5", + bg: "[#E6E8FF]", + color: "[#3F479D]", + rounded: "xs", + fontVariantNumeric: "tabular-nums", + userSelect: "none", + flexShrink: "1", + minW: "0", + maxW: "full", + overflowWrap: "anywhere", + wordBreak: "break-word", + whiteSpace: "normal", + textAlign: "right", + fontSize: "[11px]", + fontWeight: "bold", + letterSpacing: "wide", +}); + +const textItemSplitHighlight = { + bg: "[#D9DCFF]", + boxShadow: "[inset 0 0 0 1px #8A92D8]", +} as const; + +const textItemConfirmingSplitHighlight = { + bg: "[#C8CDF8]", + boxShadow: "[inset 0 0 0 2px #6F78CE]", +} as const; + +const textItemStyle = css({ + display: "flex", + alignItems: "flex-start", + gap: "1", + px: "1.5", + py: "0.5", + bg: "bg.primary-alternative", + rounded: "xs", + minW: "0", + w: "full", + boxSizing: "border-box", + "&:hover": textItemSplitHighlight, +}); + +const textItemDraggingStyle = css({ opacity: "0.35" }); +const textItemPendingSplitStyle = css(textItemSplitHighlight); +const textItemConfirmingSplitStyle = css({ + ...textItemConfirmingSplitHighlight, + "&:hover": textItemConfirmingSplitHighlight, +}); + +const textContextMenu = css({ + position: "fixed", + zIndex: "60", + minW: "[150px]", + bg: "bg.primary", + border: "[1px solid #BCBAB8]", + rounded: "sm", + boxShadow: "[0_8px_20px_rgba(17,0,65,0.14)]", + p: "1", +}); + +const textContextMenuButton = css({ + display: "flex", + alignItems: "center", + gap: "1.5", + w: "full", + px: "2", + py: "1.5", + bg: "transparent", + border: "none", + rounded: "xs", + cursor: "pointer", + color: "text.default", + textStyle: "label.md.default", + textAlign: "left", + "&:hover": { + bg: "[#D9DCFF]", + color: "[#1B0D58]", + }, + "&:focus-visible": { + outline: "[2px solid #3F479D]", + outlineOffset: "[1px]", + }, +}); + +const groupCardStyle = css({ + bg: "bg.primary", + border: "[1px solid #BCBAB8]", + rounded: "sm", + minW: "0", + w: "full", +}); + +// Used for both drag-over drop target and hover — same inset outline + violet bg +const groupHoverStyle = css({ + bg: "bg.primary-alternative", + boxShadow: "[inset 0 0 0 2px #3F479D]", +}); + +const duplicateBadge = css({ + display: "inline-flex", + alignItems: "center", + gap: "1", + px: "1.5", + py: "0.5", + bg: "[#ECEEFF]", + color: "[#3F479D]", + border: "[1px solid #7B84D4]", + rounded: "xs", + fontSize: "[11px]", + fontWeight: "semibold", + cursor: "pointer", + maxW: "full", + minW: "0", + overflow: "hidden", + "& > span": { + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }, + "&:hover": { bg: "[#D8DBFF]" }, +}); + +const groupHeader = css({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + gap: "1", + minW: "0", +}); + +const groupHeaderLeft = css({ + display: "flex", + alignItems: "center", + minW: "0", + flex: "1", + overflow: "hidden", +}); + +interface DraggableTextChipProps { + canonicalId: string; + normalizedText: string; + displayText: string; + onRemove: () => void; + onCreateGroup: (position: { x: number; y: number }) => void; + canCreateGroup: boolean; + isSplitTarget: boolean; + isSplitActionHovered: boolean; +} + +function DraggableTextChip({ + canonicalId, + normalizedText, + displayText, + onRemove, + onCreateGroup, + canCreateGroup, + isSplitTarget, + isSplitActionHovered, +}: DraggableTextChipProps) { + const { attributes, listeners, setNodeRef, isDragging } = useDraggable({ + id: textDndId(canonicalId, normalizedText), + data: { type: "text", canonicalId, text: normalizedText, displayText }, + }); + + return ( +
{ + if (!canCreateGroup) return; + event.preventDefault(); + event.stopPropagation(); + onCreateGroup({ x: event.clientX, y: event.clientY }); + }} + > + + + {displayText} + + +
+ ); +} + +function TextChipOverlay({ text }: { text: string }) { + return ( +
+ + {text} +
+ ); +} + +interface LabelEntityTabProps { + expandedGroups: Record; + expandedSections: Record; + onDerivedGroupRemove: (canonicalId: string) => void; + onDerivedValueRemove: (canonicalId: string, value: string) => void; + onDerivedLabelChange: ( + canonicalId: string, + labelId: string | undefined, + ) => void; + onGroupOpenChange: (canonicalId: string, open: boolean) => void; + onSectionOpenChange: (section: string, open: boolean) => void; +} + +export default function LabelEntityTab({ + expandedGroups, + expandedSections, + onDerivedGroupRemove, + onDerivedValueRemove, + onDerivedLabelChange, + onGroupOpenChange, + onSectionOpenChange, +}: LabelEntityTabProps) { + const files = useFiles(); + const dispatch = useFileDispatch(); + const { tags, words } = useExcludedTagsConfig(); + const activeFiles = useMemo( + () => + files.map((file) => ({ + ...file, + predictions: filterActivePredictions(file.predictions, tags, words), + })), + [files, tags, words], + ); + const groups = useEntityGroups(activeFiles); + const { + hoveredCanonicalId, + lastEditedCanonicalId, + setHoveredCanonicalId, + setLastEditedCanonicalId, + } = useHoverState(); + + const { groupOrder } = useGroupOrder(); + const { setGroupOrder } = useGroupOrderActions(); + + const [pendingRemoval, setPendingRemoval] = useState<{ + canonicalId: string; + label: string; + } | null>(null); + const [pendingMerge, setPendingMerge] = useState<{ + source: EntityGroup; + target: EntityGroup; + } | null>(null); + const [activeTextDrag, setActiveTextDrag] = useState<{ + normalizedText: string; + displayText: string; + fromCanonicalId: string; + } | null>(null); + const [overGroupId, setOverGroupId] = useState(null); + const [textMenu, setTextMenu] = useState<{ + x: number; + y: number; + canonicalId: string; + normalizedText: string; + } | null>(null); + const [textMenuActionHovered, setTextMenuActionHovered] = useState(false); + + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + const groupsByCategory = useMemo(() => { + const result: Record = Object.fromEntries( + ANONYMIZER_CATEGORY_NAMES.map((c) => [c, []]), + ); + for (const g of groups) { + const cat = getAnonymizerCategoryForLabel(g.renderBase); + result[cat] ??= []; + result[cat].push(g); + } + return result; + }, [groups]); + + useEffect(() => { + if (!textMenu) return; + + const close = () => { + setTextMenu(null); + setTextMenuActionHovered(false); + }; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") close(); + }; + + window.addEventListener("click", close); + window.addEventListener("contextmenu", close); + window.addEventListener("keydown", handleKeyDown); + window.addEventListener("scroll", close, true); + + return () => { + window.removeEventListener("click", close); + window.removeEventListener("contextmenu", close); + window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener("scroll", close, true); + }; + }, [textMenu]); + + function getOrderedGroups(category: string): EntityGroup[] { + const catGroups = groupsByCategory[category] ?? []; + const order = groupOrder?.[category]; + if (!order) return catGroups; + const byId = new Map(catGroups.map((g) => [g.canonicalId, g])); + const ordered: EntityGroup[] = []; + for (const id of order) { + const g = byId.get(id); + if (g) { + ordered.push(g); + byId.delete(id); + } + } + for (const g of byId.values()) ordered.push(g); + return ordered; + } + + function handleDragStart(event: DragStartEvent) { + const data = event.active.data.current; + if (data?.type === "text") { + setActiveTextDrag({ + normalizedText: data.text, + displayText: data.displayText ?? data.text, + fromCanonicalId: data.canonicalId, + }); + } + } + + function handleDragOver(event: DragOverEvent) { + if (!activeTextDrag) { + setOverGroupId(null); + return; + } + const overId = event.over?.id as string | undefined; + if (!overId) { + setOverGroupId(null); + return; + } + const groupId = + parseGroupDndId(overId) ?? parseTextDndId(overId)?.canonicalId ?? null; + setOverGroupId(groupId); + } + + function handleDragEnd(event: DragEndEvent) { + const { active, over } = event; + setActiveTextDrag(null); + setOverGroupId(null); + if (!over) return; + + const activeId = active.id as string; + const overId = over.id as string; + const textParsed = parseTextDndId(activeId); + + if (textParsed) { + const targetCanonicalId = + parseGroupDndId(overId) ?? parseTextDndId(overId)?.canonicalId ?? null; + if (!targetCanonicalId || targetCanonicalId === textParsed.canonicalId) + return; + const sourceGroup = groups.find( + (g) => g.canonicalId === textParsed.canonicalId, + ); + const targetGroup = groups.find( + (g) => g.canonicalId === targetCanonicalId, + ); + if (!sourceGroup || !targetGroup) return; + for (const mention of sourceGroup.mentions.filter( + (m) => normalizeText(m.text) === normalizeText(textParsed.text), + )) { + dispatch( + moveMentionToGroup( + mention.mentionId, + targetGroup.canonicalId, + targetGroup.renderBase as AllLabels | AllLabelsWithSufix, + ), + ); + } + setLastEditedCanonicalId(targetGroup.canonicalId); + return; + } + + const groupParsed = parseGroupDndId(activeId); + if (groupParsed) { + const targetCanonicalId = parseGroupDndId(overId); + if (!targetCanonicalId || targetCanonicalId === groupParsed) return; + const sourceGroup = groups.find((g) => g.canonicalId === groupParsed); + if (!sourceGroup) return; + const category = getAnonymizerCategoryForLabel(sourceGroup.renderBase); + if (!category) return; + const orderedGroups = getOrderedGroups(category); + const ids = orderedGroups.map((g) => g.canonicalId); + const oldIdx = ids.indexOf(groupParsed); + const newIdx = ids.indexOf(targetCanonicalId); + if (oldIdx === -1 || newIdx === -1) return; + setGroupOrder({ + ...groupOrder, + [category]: arrayMove(ids, oldIdx, newIdx), + }); + } + } + + function handleLabelChange(canonicalId: string, labelId: string | undefined) { + if (!labelId) return; + dispatch( + updatePredictionsByCanonicalId( + canonicalId, + labelId as AllLabels | AllLabelsWithSufix, + ), + ); + onDerivedLabelChange(canonicalId, labelId); + setLastEditedCanonicalId(canonicalId); + } + + function handleRemoveValue(canonicalId: string, normalizedText: string) { + dispatch(removePredictionValueByCanonicalId(canonicalId, normalizedText)); + onDerivedValueRemove(canonicalId, normalizedText); + } + + function handleCreateGroupFromText() { + if (!textMenu) return; + + const sourceGroup = groups.find( + (group) => group.canonicalId === textMenu.canonicalId, + ); + if (!sourceGroup) { + setTextMenu(null); + setTextMenuActionHovered(false); + return; + } + + const targetCanonicalId = crypto.randomUUID(); + const targetLabel = sourceGroup.renderBase as + | AllLabels + | AllLabelsWithSufix; + const mentionsToSplit = sourceGroup.mentions.filter( + (mention) => normalizeText(mention.text) === textMenu.normalizedText, + ); + if (mentionsToSplit.length === 0) { + setTextMenu(null); + setTextMenuActionHovered(false); + return; + } + + for (const mention of mentionsToSplit) { + dispatch( + moveMentionToGroup(mention.mentionId, targetCanonicalId, targetLabel), + ); + } + + const category = getAnonymizerCategoryForLabel(sourceGroup.renderBase); + const existingOrder = groupOrder?.[category]; + if (existingOrder) { + const sourceIndex = existingOrder.indexOf(sourceGroup.canonicalId); + const nextOrder = existingOrder.filter((id) => id !== targetCanonicalId); + nextOrder.splice( + sourceIndex === -1 ? nextOrder.length : sourceIndex + 1, + 0, + targetCanonicalId, + ); + setGroupOrder({ ...groupOrder, [category]: nextOrder }); + } + + setLastEditedCanonicalId(targetCanonicalId); + setTextMenu(null); + setTextMenuActionHovered(false); + } + + function confirmRemoval() { + if (!pendingRemoval) return; + dispatch(removePredictionsByCanonicalId(pendingRemoval.canonicalId)); + onDerivedGroupRemove(pendingRemoval.canonicalId); + setPendingRemoval(null); + } + + function confirmMerge() { + if (!pendingMerge) return; + dispatch( + mergeGroups( + pendingMerge.source.canonicalId, + pendingMerge.target.canonicalId, + pendingMerge.target.renderBase as AllLabels | AllLabelsWithSufix, + ), + ); + setPendingMerge(null); + setLastEditedCanonicalId(null); + } + + return ( + <> + { + if (!open) setPendingRemoval(null); + }} + onConfirm={confirmRemoval} + /> + setPendingMerge(null)} + onConfirm={confirmMerge} + /> + + + {textMenu && ( +
event.stopPropagation()} + onContextMenu={(event) => { + event.preventDefault(); + event.stopPropagation(); + }} + onPointerEnter={() => setTextMenuActionHovered(true)} + onPointerLeave={() => setTextMenuActionHovered(false)} + > + +
+ )} + + + {ANONYMIZER_CATEGORY_NAMES.map((category, i) => { + const orderedGroups = getOrderedGroups(category); + if (orderedGroups.length === 0) return null; + const labelOptions = getLabelsPrioritizingCategory(category); + + return ( + + {i > 0 && } + onSectionOpenChange(category, open)} + > + + + groupDndId(g.canonicalId), + )} + strategy={verticalListSortingStrategy} + > + {orderedGroups.map((group) => { + const isDropTarget = + activeTextDrag !== null && + overGroupId === group.canonicalId; + const duplicatePrimary = group.duplicateOf + ? groups.find( + (g) => g.canonicalId === group.duplicateOf, + ) + : null; + + // When the user recently edited THIS group and it became a primary with a + // duplicate pointing to it, show the badge on this group instead. + const reverseDuplicate = + !group.isDuplicate && + group.canonicalId === lastEditedCanonicalId + ? (groups.find( + (g) => + g.isDuplicate && + g.duplicateOf === group.canonicalId, + ) ?? null) + : null; + const badgeTarget = + reverseDuplicate ?? duplicatePrimary; + const isGroupOpen = + expandedGroups[group.canonicalId] ?? true; + + return ( + + onGroupOpenChange(group.canonicalId, !isGroupOpen) + } + toggleLabel={ + isGroupOpen + ? `Colapsar ${group.renderToken}` + : `Expandir ${group.renderToken}` + } + > + + setHoveredCanonicalId(group.canonicalId) + } + onMouseLeave={() => setHoveredCanonicalId(null)} + > +
+
+ {badgeTarget && ( + + + + + + + Grupo idéntico a{" "} + {badgeTarget.renderToken}. Clic para + unificar. + + + + )} +
+ + {group.renderToken} + +
+ + +
+ + handleLabelChange( + group.canonicalId, + opt.id, + ) + } + /> +
+ +
+ + {isGroupOpen && ( + + {group.uniqueTexts.flatMap((normText, idx) => + group.displayTexts[idx].map((verbatim) => ( + + handleRemoveValue( + group.canonicalId, + normText, + ) + } + canCreateGroup={ + group.mentions.length > 1 + } + onCreateGroup={(position) => { + setTextMenu({ + ...position, + canonicalId: group.canonicalId, + normalizedText: normText, + }); + setTextMenuActionHovered(false); + }} + isSplitTarget={ + textMenu?.canonicalId === + group.canonicalId && + textMenu.normalizedText === normText + } + isSplitActionHovered={ + textMenuActionHovered + } + /> + )), + )} + {group.uniqueTexts.length === 0 && ( + + Sin menciones + + )} + + )} +
+
+ ); + })} +
+
+
+
+ ); + })} +
+ + + {activeTextDrag && ( + + )} + +
+ + ); +} diff --git a/frontend/src/renderer/src/components/anonymizer/label-manager/index.tsx b/frontend/src/renderer/src/components/anonymizer/label-manager/index.tsx new file mode 100644 index 00000000..f556ca9a --- /dev/null +++ b/frontend/src/renderer/src/components/anonymizer/label-manager/index.tsx @@ -0,0 +1,190 @@ +import { useState } from "react"; + +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { useFileDispatch } from "@/hooks"; +import { + removePredictionValueByCanonicalId, + removePredictionsByCanonicalId, + updatePredictionsByCanonicalId, +} from "@/reducers/file/actions"; +import { css, sva } from "@/styled/css"; +import { HStack } from "@/styled/jsx"; +import { stack } from "@/styled/patterns"; +import type { AllLabels, AllLabelsWithSufix } from "@/types/aymurai"; +import LabelConfigTab from "./config-tab"; +import LabelEntityTab from "./entity-tab"; +import LabelManagerTab from "./tab"; + +type ExpandedState = Record; + +const styles = sva({ + slots: ["container", "header", "body", "close"], + base: { + container: { + ...stack.raw({ gap: "0" }), + width: "[400px]", + maxWidth: "[400px]", + h: "full", + minH: "0", + flexShrink: "0", + overflow: "hidden", + overflowX: "hidden", + + bg: "bg.primary", + }, + header: { + px: "5", + py: "6", + pb: "4", + flexShrink: "0", + bg: "bg.primary", + borderBottom: "[1px solid #BCBAB8]", + zIndex: "1", + }, + body: { + px: "5", + pt: "6", + pb: "6", + flex: "1", + minH: "0", + overflowY: "auto", + overflowX: "hidden", + }, + close: { + cursor: "pointer", + }, + }, +}); + +const tooltipContent = css({ + bg: "action.hover", + color: "white", + px: "1.5", + py: "0.5", + rounded: "sm", + fontSize: "[12px]", + boxShadow: "[none]", +}); + +interface LabelManagerProps { + onClose: () => void; +} +export default function LabelManager({ onClose }: LabelManagerProps) { + const [selectedTab, setSelectedTab] = useState<"entity" | "config">("entity"); + const [expandedEntitySections, setExpandedEntitySections] = + useState({}); + const [expandedEntityGroups, setExpandedEntityGroups] = + useState({}); + const [expandedConfigSections, setExpandedConfigSections] = + useState({}); + const dispatch = useFileDispatch(); + + const classes = styles(); + + function handleDerivedGroupRemove(canonicalId: string) { + dispatch(removePredictionsByCanonicalId(canonicalId)); + } + + function handleDerivedValueRemove(canonicalId: string, value: string) { + dispatch(removePredictionValueByCanonicalId(canonicalId, value)); + } + + function handleDerivedLabelChange( + canonicalId: string, + labelId: string | undefined, + ) { + if (!labelId) return; + dispatch( + updatePredictionsByCanonicalId( + canonicalId, + labelId as AllLabels | AllLabelsWithSufix, + ), + ); + } + + function handleEntitySectionOpenChange(section: string, open: boolean) { + setExpandedEntitySections((prev) => ({ ...prev, [section]: open })); + } + + function handleEntityGroupOpenChange(canonicalId: string, open: boolean) { + setExpandedEntityGroups((prev) => ({ ...prev, [canonicalId]: open })); + } + + function handleConfigSectionOpenChange(section: string, open: boolean) { + setExpandedConfigSections((prev) => ({ ...prev, [section]: open })); + } + + return ( +
+ + + + + + setSelectedTab("entity")} + > + Entidades + + + + Revisar, editar y organizar los grupos de entidades reconocidos + + + + + setSelectedTab("config")} + > + Configuración + + + + Elegir qué categorías anonimizar y definir términos excluidos + + + + + + + + + Cerrar gestor de etiquetas + + + + +
+ {selectedTab === "entity" ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/frontend/src/renderer/src/components/anonymizer/label-manager/label.tsx b/frontend/src/renderer/src/components/anonymizer/label-manager/label.tsx new file mode 100644 index 00000000..45b25748 --- /dev/null +++ b/frontend/src/renderer/src/components/anonymizer/label-manager/label.tsx @@ -0,0 +1,40 @@ +import { css } from "@/styled/css"; +import { HStack, styled } from "@/styled/jsx"; +import { XCircle } from "phosphor-react"; + +interface LabelProps { + children: string | string[]; + onRemove: () => void; +} +export function Label({ children, onRemove }: LabelProps) { + return ( + + {children} + + + ); +} diff --git a/frontend/src/renderer/src/components/anonymizer/label-manager/merge-dialog.tsx b/frontend/src/renderer/src/components/anonymizer/label-manager/merge-dialog.tsx new file mode 100644 index 00000000..f84a1ce0 --- /dev/null +++ b/frontend/src/renderer/src/components/anonymizer/label-manager/merge-dialog.tsx @@ -0,0 +1,48 @@ +import Button from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogTitle, +} from "@/components/ui/dialog"; +import type { EntityGroup } from "@/hooks/useEntityGroups"; + +interface MergeDialogProps { + source: EntityGroup | null; + target: EntityGroup | null; + onClose: () => void; + onConfirm: () => void; +} + +export default function MergeDialog({ + source, + target, + onClose, + onConfirm, +}: MergeDialogProps) { + return ( + { + if (!open) onClose(); + }} + > + + Unificar grupos + {source && target && ( +

+ ¿Deseas unificar {source.renderToken} con{" "} + {target.renderToken}?
+ Todas las menciones de {source.renderToken} pasarán a + pertenecer a {target.renderToken} y el grupo original será + eliminado. +

+ )} + + + + +
+
+ ); +} diff --git a/frontend/src/renderer/src/components/anonymizer/label-manager/remove-dialog.tsx b/frontend/src/renderer/src/components/anonymizer/label-manager/remove-dialog.tsx new file mode 100644 index 00000000..c6d82475 --- /dev/null +++ b/frontend/src/renderer/src/components/anonymizer/label-manager/remove-dialog.tsx @@ -0,0 +1,36 @@ +import Button from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogTitle, +} from "@/components/ui/dialog"; + +interface RemoveDialogProps { + isOpen: boolean; + label: string; + onClose: (open: boolean) => void; + onConfirm: () => void; +} +export default function RemoveDialog({ + isOpen, + onClose, + onConfirm, + label, +}: RemoveDialogProps) { + return ( + + + Eliminar todas las ocurrencias +

+ ¿Deseas eliminar todas las ocurrencias del grupo con la etiqueta{" "} + {label}? +

+ + + + +
+
+ ); +} diff --git a/frontend/src/renderer/src/components/anonymizer/label-manager/section.tsx b/frontend/src/renderer/src/components/anonymizer/label-manager/section.tsx new file mode 100644 index 00000000..6055e2e1 --- /dev/null +++ b/frontend/src/renderer/src/components/anonymizer/label-manager/section.tsx @@ -0,0 +1,76 @@ +import { useState } from "react"; + +import { css } from "@/styled/css"; +import { HStack, Stack, styled } from "@/styled/jsx"; +import { CaretUp } from "phosphor-react"; + +const header = css({ + cursor: "pointer", +}); + +const caret = css({ + transition: "transform", + transitionDuration: "[0.2s]", + transitionTimingFunction: "[ease]", +}); + +const contentOuter = css({ + display: "grid", + transition: "[grid-template-rows]", + transitionDuration: "[0.3s]", + transitionTimingFunction: "[ease]", +}); + +const contentInner = css({ + minHeight: "[0]", + overflow: "hidden", +}); + +interface SectionProps { + children: React.ReactNode; + open?: boolean; + onOpenChange?: (open: boolean) => void; + title: string; + headerAction?: React.ReactNode; +} +export default function LabelManagerSection({ + open: controlledOpen, + onOpenChange, + title: sectionTitle, + children, + headerAction, +}: SectionProps) { + const [uncontrolledOpen, setUncontrolledOpen] = useState(true); + const open = controlledOpen ?? uncontrolledOpen; + + function toggleOpen() { + const nextOpen = !open; + onOpenChange?.(nextOpen); + if (controlledOpen === undefined) setUncontrolledOpen(nextOpen); + } + + return ( + + + +
+ +
+ + {sectionTitle} + +
+ {headerAction} +
+
+
{children}
+
+
+ ); +} diff --git a/frontend/src/renderer/src/components/anonymizer/label-manager/sortable-group.tsx b/frontend/src/renderer/src/components/anonymizer/label-manager/sortable-group.tsx new file mode 100644 index 00000000..988657f8 --- /dev/null +++ b/frontend/src/renderer/src/components/anonymizer/label-manager/sortable-group.tsx @@ -0,0 +1,103 @@ +import { css } from "@/styled/css"; +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { DotsSixVertical } from "phosphor-react"; +import { type PointerEvent, type ReactNode, useRef } from "react"; + +const handle = css({ + cursor: "grab", + color: "text.lighter", + display: "flex", + alignItems: "center", + p: "0", + bg: "transparent", + border: "none", + "&:hover": { color: "text.default" }, + "&:active": { cursor: "grabbing" }, +}); + +const wrapper = css({ + display: "flex", + alignItems: "flex-start", + gap: "2", + minW: "0", + w: "full", + transition: "[opacity 0.2s, box-shadow 0.2s]", +}); + +const content = css({ + flex: "1", + minW: "0", +}); + +interface SortableGroupProps { + id: string; + children: ReactNode; + isOpen?: boolean; + onToggleOpen?: () => void; + toggleLabel?: string; +} + +export default function SortableGroup({ + id, + children, + isOpen, + onToggleOpen, + toggleLabel, +}: SortableGroupProps) { + const pointerStartRef = useRef<{ x: number; y: number } | null>(null); + const movedRef = useRef(false); + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + boxShadow: isDragging ? "0 4px 12px rgba(0,0,0,0.15)" : "none", + }; + + function handlePointerDown(event: PointerEvent) { + pointerStartRef.current = { x: event.clientX, y: event.clientY }; + movedRef.current = false; + } + + function handlePointerMove(event: PointerEvent) { + const pointerStart = pointerStartRef.current; + if (!pointerStart) return; + + const dx = event.clientX - pointerStart.x; + const dy = event.clientY - pointerStart.y; + if (Math.hypot(dx, dy) > 4) movedRef.current = true; + } + + function handleClick() { + if (movedRef.current) return; + onToggleOpen?.(); + } + + return ( +
+ +
{children}
+
+ ); +} diff --git a/frontend/src/renderer/src/components/anonymizer/label-manager/tab.tsx b/frontend/src/renderer/src/components/anonymizer/label-manager/tab.tsx new file mode 100644 index 00000000..926897e2 --- /dev/null +++ b/frontend/src/renderer/src/components/anonymizer/label-manager/tab.tsx @@ -0,0 +1,33 @@ +import { Tab } from "@/components/tabs"; +import { css } from "@/styled/css"; +import { type ButtonHTMLAttributes, forwardRef } from "react"; + +const button = css({ + cursor: "pointer", +}); + +interface LabelManagerTabProps + extends Omit, "children"> { + children: string; + onClick: () => void; + isSelected: boolean; +} +const LabelManagerTab = forwardRef( + function LabelManagerTab({ onClick, children, isSelected, ...props }, ref) { + return ( + + ); + }, +); + +export default LabelManagerTab; diff --git a/frontend/src/renderer/src/components/arrow/LeftArrow.tsx b/frontend/src/renderer/src/components/arrow/LeftArrow.tsx new file mode 100644 index 00000000..5ba65537 --- /dev/null +++ b/frontend/src/renderer/src/components/arrow/LeftArrow.tsx @@ -0,0 +1,13 @@ +export default function LeftArrow() { + return ( + + + + ); +} diff --git a/frontend/src/renderer/src/components/arrow/RightArrow.tsx b/frontend/src/renderer/src/components/arrow/RightArrow.tsx new file mode 100644 index 00000000..446b8a6f --- /dev/null +++ b/frontend/src/renderer/src/components/arrow/RightArrow.tsx @@ -0,0 +1,16 @@ +export default function RightArrow() { + return ( + + + + ); +} diff --git a/frontend/src/renderer/src/components/arrow/index.ts b/frontend/src/renderer/src/components/arrow/index.ts new file mode 100644 index 00000000..e1939cb8 --- /dev/null +++ b/frontend/src/renderer/src/components/arrow/index.ts @@ -0,0 +1,9 @@ +import LeftArrow from "./LeftArrow"; +import RightArrow from "./RightArrow"; + +const Arrow = { + Right: RightArrow, + Left: LeftArrow, +}; + +export default Arrow; diff --git a/frontend/src/renderer/src/components/brand/built-by.tsx b/frontend/src/renderer/src/components/brand/built-by.tsx new file mode 100644 index 00000000..dd7a4a79 --- /dev/null +++ b/frontend/src/renderer/src/components/brand/built-by.tsx @@ -0,0 +1,29 @@ +import { DATAGENERO_URL } from "@/constants/config"; +import { styled } from "@/styled/jsx"; +import { type StackStyles, stack } from "@/styled/patterns"; +import { useTranslation } from "react-i18next"; + +interface BuiltByProps { + size?: number; + gap?: StackStyles["gap"]; +} +export default function BuiltBy({ size = 150, gap = "2" }: BuiltByProps) { + const { t } = useTranslation(); + return ( + + + {t("platformBuiltBy")} + + DataGenero isologo + + ); +} diff --git a/frontend/src/renderer/src/components/checkbox/checkbox-group/CheckboxGroup.styles.ts b/frontend/src/renderer/src/components/checkbox/checkbox-group/CheckboxGroup.styles.ts new file mode 100644 index 00000000..f727702b --- /dev/null +++ b/frontend/src/renderer/src/components/checkbox/checkbox-group/CheckboxGroup.styles.ts @@ -0,0 +1,30 @@ +import { styled } from "@/styles"; + +export const Legend = styled("legend", { + fontWeight: "$default", + fontSize: "$subtitleSm", + lineHeight: "$subtitleSm", + color: "$textLighter", + + mb: "$xs", +}); + +export const Group = styled("fieldset", { + display: "flex", + gap: "$m", + flexWrap: "wrap", + + variants: { + direction: { + horizontal: { + flexDirection: "row", + }, + vertical: { + flexDirection: "column", + }, + }, + }, + defaultVariants: { + direction: "horizontal", + }, +}); diff --git a/frontend/src/renderer/src/components/checkbox/checkbox-group/index.tsx b/frontend/src/renderer/src/components/checkbox/checkbox-group/index.tsx new file mode 100644 index 00000000..93d4fc96 --- /dev/null +++ b/frontend/src/renderer/src/components/checkbox/checkbox-group/index.tsx @@ -0,0 +1,39 @@ +import { + Children, + type ReactElement, + type ReactNode, + cloneElement, + isValidElement, +} from "react"; + +import type { Props as CheckboxProps } from "../checkbox"; +import { Group, Legend } from "./CheckboxGroup.styles"; + +interface Props { + children: ReactNode; + title?: string; + name: string; + direction?: "horizontal" | "vertical"; +} +export default function CheckboxGroup({ + title, + name, + direction = "horizontal", + children, +}: Props) { + // Inject the name to the children + const checkboxWithName = Children.map(children, (child) => { + if (isValidElement(child)) { + return cloneElement(child as ReactElement, { + name, + }); + } + }); + + return ( + + {title && {title}} + {checkboxWithName} + + ); +} diff --git a/frontend/src/renderer/src/components/checkbox/checkbox/Checkbox.styles.ts b/frontend/src/renderer/src/components/checkbox/checkbox/Checkbox.styles.ts new file mode 100644 index 00000000..c29cec78 --- /dev/null +++ b/frontend/src/renderer/src/components/checkbox/checkbox/Checkbox.styles.ts @@ -0,0 +1,143 @@ +import { styled } from "@/styles"; + +export const Input = styled("input", { + position: "absolute", + top: 0, + left: 0, + + opacity: 0, +}); + +export const Checkbox = styled("div", { + display: "flex", + alignItems: "center", + justifyContent: "center", + + width: 18, + height: 18, + p: 1, + + borderRadius: "$xs", + boxSizing: "border-box", +}); + +export const Wrapper = styled("label", { + // Unchecked + $$checkbox_bg_default: "$colors$white", + $$checkbox_border_default: "$sizes$xxs solid $colors$actionDefaultAlt", + // hover + $$checkbox_bgHover_default: "$colors$white", + $$checkbox_borderHover_default: "1px solid $colors$borderPrimary", + // disabled + $$checkbox_bgDisabled_default: "$colors$actionDisabled", + $$checkbox_borderDisabled_default: "$sizes$xxs solid $colors$borderPrimary", + // focus + $$checkbox_bgFocus_default: "$colors$white", + $$checkbox_borderFocus_default: "1px solid $colors$borderPrimary", + + // Checked + $$checkbox_bg_checked: "$colors$actionDefaultAlt", + $$checkbox_border_checked: "none", + // hover + $$checkbox_bgHover_checked: "$colors$actionHover", + $$checkbox_borderHover_checked: "none", + // disabled + $$checkbox_bgDisabled_checked: "$colors$actionDisabled", + $$checkbox_borderDisabled_checked: "none", + // focus + $$checkbox_bgFocus_checked: "$colors$actionDefaultAlt", + $$checkbox_borderFocus_checked: "none", + + // Focus + $$checkbox_outlineFocus_noText: "3px solid $colors$borderPrimaryAlt", + $$checkbox_outlineFocus_Text: "3px solid $colors$borderPrimary", + + position: "relative", + + display: "flex", + flexDirection: "row", + alignItems: "center", + gap: "$s", + + transitionDuration: "$transitions$s", + transitionProperty: "outline, border-radius, border", + transitionTimingFunction: "ease", + + variants: { + hasText: { + true: { + p: "$s", + "&:focus-within": { + outline: "$$checkbox_outlineFocus_Text", + borderRadius: "3px", + }, + }, + false: { + "&:focus-within": { + outline: "$$checkbox_outlineFocus_noText", + borderRadius: "$xs", + }, + // Focus for checkbox + [`& > input:checked:focus-visible + ${Checkbox}`]: { + bg: "$$checkbox_bgFocus_checked", + b: "$$checkbox_borderFocus_checked", + }, + [`& > input:focus-visible + ${Checkbox}`]: { + bg: "$$checkbox_bgFocus_default", + b: "$$checkbox_borderFocus_default", + }, + }, + }, + isDisabled: { + true: { + "&, & > *": { + cursor: "not-allowed", + }, + }, + false: { + "&, & > *": { + cursor: "pointer", + }, + }, + }, + }, + defaultVariants: { + hasText: false, + isDisabled: false, + }, + + // Checkbox styles + [`& > ${Checkbox}`]: { + b: "$$checkbox_border_default", + bg: "$$checkbox_bg_default", + }, + [`& > input:checked + ${Checkbox}`]: { + bg: "$$checkbox_bg_checked", + border: "$$checkbox_border_checked", + }, + + // Hide SVG when unchecked + [`& > input:not(:checked) + ${Checkbox} svg`]: { + display: "none", + }, + + // Hover + [`&:hover > ${Checkbox}`]: { + b: "$$checkbox_borderHover_default", + bg: "$$checkbox_bgHover_default", + }, + [`&:hover > input:checked + ${Checkbox}`]: { + b: "$$checkbox_borderHover_checked", + bg: "$$checkbox_bgHover_checked", + }, + + // Disabled + [`& > input:disabled + ${Checkbox}`]: { + bg: "$$checkbox_bgDisabled_default", + b: "$$checkbox_borderDisabled_default", + }, + [`& > input:checked:disabled + ${Checkbox}`]: { + bg: "$$checkbox_bgDisabled_checked", + b: "$$checkbox_borderDisabled_checked", + }, +}); diff --git a/frontend/src/renderer/src/components/checkbox/checkbox/index.tsx b/frontend/src/renderer/src/components/checkbox/checkbox/index.tsx new file mode 100644 index 00000000..37568f73 --- /dev/null +++ b/frontend/src/renderer/src/components/checkbox/checkbox/index.tsx @@ -0,0 +1,65 @@ +import { Check } from "phosphor-react"; +import { + type ChangeEventHandler, + type ReactNode, + forwardRef, + useImperativeHandle, + useState, +} from "react"; + +import type { CSS } from "@/styles"; +import { colors } from "@/styles/tokens"; +import { Input, Checkbox as StyledCheckbox, Wrapper } from "./Checkbox.styles"; + +export interface Props { + children?: ReactNode; + disabled?: boolean; + checked?: boolean; + onChange?: (value: boolean) => void; + name?: string; + css?: CSS; +} +export default forwardRef<{ value: boolean }, Props>(function Checkbox( + { disabled = false, checked = false, name, onChange, children, css }, + ref, +) { + const [isChecked, setIsChecked] = useState(checked); + + const hasText = !!children; + + // Only exposes `value` object to the parent component + useImperativeHandle( + ref, + () => { + return { + value: isChecked, + }; + }, + [isChecked], + ); + + const handleToggle: ChangeEventHandler = (e) => { + setIsChecked(e.target.checked); + onChange?.(isChecked); + }; + + const iconColor = disabled + ? colors.textOnButtonDisabled + : colors.textOnButtonAlternative; + + return ( + + + + + + {children} + + ); +}); diff --git a/frontend/src/renderer/src/components/checkbox/index.ts b/frontend/src/renderer/src/components/checkbox/index.ts new file mode 100644 index 00000000..eebc899c --- /dev/null +++ b/frontend/src/renderer/src/components/checkbox/index.ts @@ -0,0 +1,4 @@ +import Checkbox from "./checkbox"; +import CheckboxGroup from "./checkbox-group"; + +export { Checkbox, CheckboxGroup }; diff --git a/frontend/src/renderer/src/components/decision-tabs/DecisionTabs.styles.ts b/frontend/src/renderer/src/components/decision-tabs/DecisionTabs.styles.ts new file mode 100644 index 00000000..0dd8c7a1 --- /dev/null +++ b/frontend/src/renderer/src/components/decision-tabs/DecisionTabs.styles.ts @@ -0,0 +1,16 @@ +import { styled } from "@/styles"; + +export const PlusButton = styled("button", { + boxShadow: "4px 0px 4px rgba(0, 0, 0, 0.05)", + + // Marked as important because of Stitches hierarchy + px: "$s !important", + py: "$s !important", + // b: '2px solid $actionDefaultAlt', + width: 48, + height: 51, + "&:disabled": { + boxShadow: "none", + color: "$textLighter", + }, +}); diff --git a/frontend/src/renderer/src/components/decision-tabs/index.tsx b/frontend/src/renderer/src/components/decision-tabs/index.tsx new file mode 100644 index 00000000..a6520c8d --- /dev/null +++ b/frontend/src/renderer/src/components/decision-tabs/index.tsx @@ -0,0 +1,52 @@ +import { Tab, TabName } from "@/components"; +import { css } from "@/styled/css"; +import { Stack } from "@/styled/jsx"; +import nArray from "@/utils/nArray"; +import { Plus } from "phosphor-react"; + +const button = css({ + cursor: "pointer", + p: "2", + display: "flex", + alignItems: "center", + justifyContent: "center", + + height: "full", + width: "12", +}); + +interface Props { + selected: number; + decisionAmount: number; + addDecision: () => void; + selectDecision: (n: number) => void; +} +export default function DecisionTabs({ + selected, + decisionAmount, + addDecision, + selectDecision, +}: Props) { + const decisionArr = nArray(decisionAmount, undefined).map((_, i) => i); + + const selectDecisionHandler = (n: number) => () => selectDecision(n); + + return ( + + {decisionArr.map((dec) => ( + + Decisión {dec + 1} + + ))} + + + ); +} diff --git a/frontend/src/renderer/src/components/dialog/index.tsx b/frontend/src/renderer/src/components/dialog/index.tsx new file mode 100644 index 00000000..16c2539c --- /dev/null +++ b/frontend/src/renderer/src/components/dialog/index.tsx @@ -0,0 +1,109 @@ +import { styled } from "@/styles"; +import { X } from "phosphor-react"; +import type { FC, ReactNode } from "react"; +import Button from "../ui/button"; + +export interface DialogOption { + id: string; + label: string; + action: () => void; +} + +interface DialogProps { + isOpen: boolean; + title?: string; + message?: string; + options?: DialogOption[]; + onClose: () => void; + children?: ReactNode; +} + +export const Overlay = styled("div", { + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "rgba(0, 0, 0, 0.5)", + display: "flex", + alignItems: "center", + justifyContent: "center", + zIndex: 1000, +}); + +export const DialogContainer = styled("div", { + backgroundColor: "$white", + borderRadius: "$xs", + padding: "$l", + minWidth: 300, + maxWidth: 700, + boxShadow: "0px 4px 8px rgba(0, 0, 0, 0.1)", +}); + +export const DialogHeader = styled("div", { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + marginBottom: "$m", +}); + +export const DialogTitle = styled("h3", { + fontSize: "$titleMd", + fontWeight: 600, + color: "$textDefault", + margin: 0, +}); + +export const DialogMessage = styled("label", { + color: "$textDefault", + marginBottom: "$l", +}); + +export const DialogButtons = styled("div", { + display: "flex", + gap: "$s", + justifyContent: "flex-end", + alignItems: "center", + marginTop: "$xl", +}); + +const Dialog: FC = ({ + isOpen, + title = "", + message, + options, + onClose, + children, +}) => { + if (!isOpen) return null; + + return ( + + e.stopPropagation()}> + + {title} + + + {message && {message}} + {children} + {options && options.length > 0 && ( + + {options.map((option, index) => ( + + ))} + + )} + + + ); +}; + +export default Dialog; diff --git a/frontend/src/renderer/src/components/drop-area.tsx b/frontend/src/renderer/src/components/drop-area.tsx new file mode 100644 index 00000000..4226066f --- /dev/null +++ b/frontend/src/renderer/src/components/drop-area.tsx @@ -0,0 +1,153 @@ +import { WHITELISTED_EXTENSIONS } from "@/constants/config"; +import { css, cva } from "@/styled/css"; +import { Stack, styled } from "@/styled/jsx"; +import { File } from "phosphor-react"; +import { useCallback, useRef, useState } from "react"; + +function FileIcon() { + return ( + + + + ); +} + +const styles = cva({ + base: { + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + gap: "8", + + w: "full", + h: "[335px]", + p: "8", + + rounded: "sm", + border: "primary", + + bg: "[#F3F4FF]", + cursor: "pointer", + + transitionProperty: "[border-color, background-color, box-shadow]", + transitionTimingFunction: "default", + transitionDuration: "normal", + }, + variants: { + dragging: { + true: { + borderColor: "brand.primary", + bg: "bg.primary-alternative", + boxShadow: "[0px 0px 15px 0px #3F479D66]", + }, + }, + }, +}); + +interface DropAreaProps { + onDropFiles: (files: File[]) => void; + title: string; + description: string; + multiple?: boolean; +} + +export default function DropArea({ + onDropFiles, + title, + description, + multiple = true, +}: DropAreaProps) { + const [dragging, setDragging] = useState(false); + const inputRef = useRef(null); + const dragCounter = useRef(0); + + const accept = WHITELISTED_EXTENSIONS.map((ext) => `.${ext}`).join(","); + + const handleFiles = useCallback( + (fileList: FileList) => { + const files = Array.from(fileList).filter((file) => { + const ext = file.name.split(".").pop()?.toLowerCase(); + return ext && WHITELISTED_EXTENSIONS.includes(ext); + }); + if (files.length > 0) onDropFiles(files); + }, + [onDropFiles], + ); + + const handleDragEnter = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + dragCounter.current++; + setDragging(true); + }, []); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + dragCounter.current--; + if (dragCounter.current === 0) setDragging(false); + }, []); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }, []); + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + dragCounter.current = 0; + setDragging(false); + if (e.dataTransfer.files.length > 0) { + handleFiles(e.dataTransfer.files); + } + }, + [handleFiles], + ); + + const handleClick = () => inputRef.current?.click(); + + const handleChange: React.ChangeEventHandler = (e) => { + if (e.target.files) handleFiles(e.target.files); + }; + + return ( +
+ + + + {title} + + + {description} + + + +
+ ); +} diff --git a/frontend/src/renderer/src/components/feature-icon.tsx b/frontend/src/renderer/src/components/feature-icon.tsx new file mode 100644 index 00000000..d83f1d5b --- /dev/null +++ b/frontend/src/renderer/src/components/feature-icon.tsx @@ -0,0 +1,65 @@ +import { cva } from "@/styled/css"; +import { stack } from "@/styled/patterns"; +import type { FeatureFlowEnum } from "@/types/features"; +import { FEATURE_ICON } from "@/utils/config"; +import type { Icon } from "phosphor-react"; + +const styles = cva({ + base: { + ...stack.raw({ align: "center", justify: "center" }), + p: "4", + borderRadius: "[14px]", + }, + variants: { + disabled: { + true: { + bg: "bg.secondary", + color: "text.lighter", + }, + false: { + color: "text.default", + bg: "bg.primary-alternative", + }, + }, + size: { + lg: { + width: "[70px]", + height: "[70px]", + p: "[14px]", + rounded: "[14px]", + }, + sm: { + width: "10", + height: "10", + p: "2", + rounded: "lg", + }, + }, + }, + defaultVariants: { + disabled: false, + size: "lg", + }, +}); + +type FeatureIconProps = { + size: "lg" | "sm"; + disabled?: boolean; +} & ( + | { feature: FeatureFlowEnum; icon?: never } + | { feature?: never; icon: Icon } +); + +export default function FeatureIcon({ + feature, + size, + icon: OverrideIcon, + disabled = false, +}: FeatureIconProps) { + const Icon = OverrideIcon ?? FEATURE_ICON[feature]; + return ( +
+ +
+ ); +} diff --git a/frontend/src/renderer/src/components/features-menu.tsx b/frontend/src/renderer/src/components/features-menu.tsx new file mode 100644 index 00000000..3a158668 --- /dev/null +++ b/frontend/src/renderer/src/components/features-menu.tsx @@ -0,0 +1,64 @@ +import { useFileDispatch } from "@/hooks/useFiles"; +import { removeAllFiles } from "@/reducers/file/actions"; +import { Grid, Stack, styled } from "@/styled/jsx"; +import { FeatureFlowEnum, featureNamespace, getFeatureRouteSlug } from "@/types/features"; +import { Link } from "@tanstack/react-router"; +import { DotsNine, Gear } from "phosphor-react"; +import { useTranslation } from "react-i18next"; +import FeatureIcon from "./feature-icon"; +import Button from "./ui/button"; +import Card from "./ui/card"; +import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; + +export default function FeaturesMenu() { + const { t } = useTranslation(); + const dispatch = useFileDispatch(); + const features = Object.values(FeatureFlowEnum); + + const handleClearFiles = () => { + dispatch(removeAllFiles()); + }; + + return ( + + + + + + + {features.map((feature) => ( + + + + + + {t("title", { ns: featureNamespace[feature] })} + + + + + ))} + + + + + + + {t("settings")} + + + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/file-annotator/FileAnnotator.styles.ts b/frontend/src/renderer/src/components/file-annotator/FileAnnotator.styles.ts new file mode 100644 index 00000000..5c825801 --- /dev/null +++ b/frontend/src/renderer/src/components/file-annotator/FileAnnotator.styles.ts @@ -0,0 +1,30 @@ +import { styled } from "@/styles"; + +export const Container = styled("div", { + flex: 1, + minWidth: 0, + // boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.1)", + + zIndex: 1, + display: "flex", + flexDirection: "column", + overflow: "hidden", +}); + +export const File = styled("div", { + flex: 1, + overflowY: "scroll", + px: "$xl", + pb: "$xl", + // pt: "$l", + + "& p, & span, & em": { + fontFamily: "$file", + fontSize: 16, + lineHeight: "160%", + }, +}); + +export const Paragraph = styled("p", { + margin: "8px 0px", +}); diff --git a/frontend/src/renderer/src/components/file-annotator/SearchBar/Counter.tsx b/frontend/src/renderer/src/components/file-annotator/SearchBar/Counter.tsx new file mode 100644 index 00000000..93c909ed --- /dev/null +++ b/frontend/src/renderer/src/components/file-annotator/SearchBar/Counter.tsx @@ -0,0 +1,66 @@ +import { css } from "@/styled/css"; +import { Stack, styled } from "@/styled/jsx"; +import { + CaretDown as NextIcon, + CaretUp as PreviousIcon, + XCircle, +} from "phosphor-react"; + +import { Button } from "@/components"; + +const counterClass = css({ + display: "flex", + alignItems: "center", + gap: "1", +}); + +interface Props { + count: number; + cursor: number; + next: () => void; + previous: () => void; + clear: () => void; + isSearching: boolean; +} + +export const Counter = ({ + count, + previous, + next, + cursor, + clear, + isSearching, +}: Props) => { + if (!isSearching) return null; + + return ( +
+ + + + + {count === 0 ? "0 ocurrencias" : `${cursor} de ${count}`} + + + + + +
+ ); +}; diff --git a/frontend/src/renderer/src/components/file-annotator/SearchBar/SearchBar.styles.ts b/frontend/src/renderer/src/components/file-annotator/SearchBar/SearchBar.styles.ts new file mode 100644 index 00000000..81c2e7eb --- /dev/null +++ b/frontend/src/renderer/src/components/file-annotator/SearchBar/SearchBar.styles.ts @@ -0,0 +1,94 @@ +import { styled } from "@/styles"; + +export const WrapperSearch = styled("div", { + display: "flex", + alignItems: "center", + flexDirection: "row", + gap: "$s", + + p: 12, + height: 48, + + bg: "$bgSecondary", + border: "1px solid $borderPrimary", + borderRadius: "$xl", + + cursor: "text", + + "&:focus-within": { + border: "1px solid $borderPrimaryAlt", + boxShadow: "0px 2px 2px rgba(0, 0, 0, 0.16)", + }, +}); + +export const ContainerLabel = styled("div", { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + gap: "$s", + width: "100%", +}); + +export const WrapperLabel = styled("div", { + width: "100%", +}); + +export const WrapperSufixLabel = styled("div", { + display: "flex", + alignItems: "center", + flexDirection: "row", + gap: "$s", + + p: 12, + height: 48, + + bg: "$bgSecondary", + border: "1px solid $borderPrimary", + + cursor: "text", + + "&:focus-within": { + border: "1px solid $borderPrimaryAlt", + boxShadow: "0px 2px 2px rgba(0, 0, 0, 0.16)", + }, +}); + +export const InputContainer = styled("div", { + display: "flex", + alignItems: "center", + gap: "$xxs", + flex: 1, +}); + +export const Input = styled("input", { + flex: 1, + + fontSize: "$labelMd", + lineHeight: "$LabelMd", + color: "$textDefault", + + p: 0, + width: "$xxl", + + border: "none", + outline: "none", +}); + +export const Counter = styled("div", { + display: "flex", + alignItems: "center", + gap: "$xxs", +}); + +export const SuggestionContainer = styled("div", { + display: "flex", + gap: "$s", +}); + +export const Suggestion = styled("mark", { + backgroundColor: "$bgSecondaryAlt", + fontFamily: "$primary", + padding: "0px $sizes$s", + borderRadius: "$s", + cursor: "pointer", +}); diff --git a/frontend/src/renderer/src/components/file-annotator/SearchBar/index.tsx b/frontend/src/renderer/src/components/file-annotator/SearchBar/index.tsx new file mode 100644 index 00000000..c8c66b2d --- /dev/null +++ b/frontend/src/renderer/src/components/file-annotator/SearchBar/index.tsx @@ -0,0 +1,198 @@ +import { + type ChangeEvent, + type KeyboardEvent, + useEffect, + useRef, + useState, +} from "react"; + +import AnonymizerLabelSelect from "@/components/anonymizer/anonymizer-label-select"; +import Button from "@/components/ui/button"; +import type { SelectOption } from "@/components/ui/select"; +import { useExcludedTagsConfig } from "@/store/useLocal"; +import { sva } from "@/styled/css"; +import { Grid, HStack, styled } from "@/styled/jsx"; +import { hstack } from "@/styled/patterns"; +import { getActiveAnonymizerLabelOptions } from "@/utils/anonymizer/labels"; +import { MagnifyingGlass } from "phosphor-react"; +import { SEARCH_MIN_LENGTH } from "../annotations"; +import { Counter } from "./Counter"; + +const searchClasses = sva({ + slots: ["searchBar", "input", "verticalHr"], + base: { + searchBar: { + ...hstack.raw({ alignItems: "center", gap: "2" }), + height: "12", + p: "3", + rounded: "3xl", + // minWidth: "[450px]", + width: "full", + border: "primary", + }, + input: { + outline: "none", + width: "full", + }, + verticalHr: { + width: "[1px]", + alignSelf: "stretch", + borderWidth: "0", + backgroundColor: "[#BCBAB8]", + height: "12", + }, + }, +}); + +interface Props { + isAnnotable?: boolean; + isLabelManagerOpen: boolean; + onSearchChange?: (value: string) => void; + onLabelChange?: (object: SelectOption | undefined) => void; + labelValue?: string; + onLabelManagerToggle: () => void; + matchesCount: number; + activeIndex: number | null; + onNext: () => void; + onPrevious: () => void; + onFocusDocument: () => void; +} + +export const SearchBar = ({ + isAnnotable = false, + isLabelManagerOpen, + onSearchChange, + onLabelChange, + labelValue, + onLabelManagerToggle, + matchesCount, + activeIndex, + onNext, + onPrevious, + onFocusDocument, +}: Props) => { + const [search, setSearch] = useState(""); + const { tags } = useExcludedTagsConfig(); + + const inputSearchRef = useRef(null); + + const classes = searchClasses(); + const labelOptions = getActiveAnonymizerLabelOptions(tags); + + useEffect(() => { + const isEditableTarget = (target: EventTarget | null) => { + if (!(target instanceof HTMLElement)) return false; + if (target === inputSearchRef.current) return false; + if (target.isContentEditable) return true; + return ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName); + }; + + const handleKeyDown = (event: globalThis.KeyboardEvent) => { + const isSearchShortcut = + (event.ctrlKey || event.metaKey) && + !event.altKey && + !event.shiftKey && + event.key.toLowerCase() === "b"; + + if (!isSearchShortcut || isEditableTarget(event.target)) return; + + event.preventDefault(); + inputSearchRef.current?.focus(); + inputSearchRef.current?.select(); + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, []); + + const changeSearchHandler = (e: ChangeEvent) => { + const text = e.target.value; + setSearch(text); + onSearchChange?.(e.target.value); + }; + + const clickSearchHandler = () => { + if (inputSearchRef.current) { + inputSearchRef.current.select(); + } + }; + + const handleClear = () => { + setSearch(""); + onSearchChange?.(""); + }; + + const handleInputKeyDown = (event: KeyboardEvent) => { + if (event.key !== "Escape") return; + event.preventDefault(); + handleClear(); + onFocusDocument(); + }; + + const searchFocus = () => inputSearchRef.current?.focus(); + + const changeLabelSelectHandler = (e: SelectOption | undefined) => { + onLabelChange?.(e); + }; + + return ( + +
+ + + + + = SEARCH_MIN_LENGTH} + /> +
+ {isAnnotable && ( + +
+ + Aplicar etiquetas + +
+ +
+ {!isLabelManagerOpen && ( + <> +
+ + + )} +
+ )} +
+ ); +}; diff --git a/frontend/src/renderer/src/components/file-annotator/annotations.ts b/frontend/src/renderer/src/components/file-annotator/annotations.ts new file mode 100644 index 00000000..de14b733 --- /dev/null +++ b/frontend/src/renderer/src/components/file-annotator/annotations.ts @@ -0,0 +1,171 @@ +import type { + AllLabels, + AllLabelsWithSufix, + PredictLabel, +} from "@/types/aymurai"; +import type { Paragraph } from "@/types/file"; +import { includes } from "@/utils/regex"; +import type { Annotation } from "./types"; + +export const SEARCH_MIN_LENGTH = 3; + +export interface SearchMatch { + id: string; + paragraphId: string; + start: number; + end: number; + index: number; +} + +/** + * Converts a predict label to an annotation + * @param labels List of predict labels from the API + * @returns List of annotations + */ +const labelToAnnotation = (labels: PredictLabel[]): Annotation[] => { + return labels.map( + ({ start_char, end_char, attrs, paragraphId, mentionId }) => ({ + start: start_char, + end: end_char, + type: "tag", + tag: attrs.aymurai_label, + paragraphId, + canonical_entity_id: attrs.canonical_entity_id, + mentionId, + }), + ); +}; + +/** + * Finds the indexes where the search string is found in the paragraph + * @param paragraph Paragraph to search in + * @param search Search string + * @returns List of indexes where the search string is found + */ +const findSearchIndexes = (paragraph: string, search: string) => { + const regex = includes(search); + + const indexes: number[] = []; + let match = regex.exec(paragraph); + + while (match) { + indexes.push(match.index); + match = regex.exec(paragraph); + } + + return indexes; +}; + +/** + * Appends search annotations to the list of label annotations + * @param arr List of label annotations + * @param search Search string + * @param paragraph Paragraph to search in + * @returns List of annotations with search annotations appended + */ +const getSearchAnnotations = ( + searchMatches: SearchMatch[], + label: AllLabels | AllLabelsWithSufix | null, + activeSearchMatchId: string | null, +): Annotation[] => { + return searchMatches.map( + (match) => + ({ + start: match.start, + end: match.end, + type: "search", + tag: label, + paragraphId: match.paragraphId, + searchMatchId: match.id, + searchIndex: match.index, + isActive: match.id === activeSearchMatchId, + }) as Annotation, + ); +}; + +/** + * Converts the predictions array into a map with the paragraph id as key + * @param predictions List of labels predicted by AymurAI + * @returns A map with the paragraph id as key and an array of predictions as value + */ +export const predictionsToMap = ( + predictions: PredictLabel[], +): Map => { + const map = new Map(); + + for (const token of predictions) { + // Ignore characters + if ([":", ",", ".", ")", "(", "-", "_"].includes(token.text)) continue; + + const paragraphPredictions = map.get(token.paragraphId); + if (paragraphPredictions) { + paragraphPredictions.push(token); + } else { + map.set(token.paragraphId, [token]); + } + } + + return map; +}; + +export const createSearchMatches = ( + paragraphs: Paragraph[], + search: string, +): SearchMatch[] => { + if (!search || search.length < SEARCH_MIN_LENGTH) return []; + + const matches: SearchMatch[] = []; + paragraphs.forEach((paragraph) => { + const indexes = findSearchIndexes(paragraph.value, search); + indexes.forEach((start) => { + const index = matches.length; + matches.push({ + id: `${paragraph.id}:${start}:${index}`, + paragraphId: paragraph.id, + start, + end: start + search.length, + index, + }); + }); + }); + + return matches; +}; + +export const searchMatchesToMap = ( + matches: SearchMatch[], +): Map => { + const map = new Map(); + + for (const match of matches) { + const paragraphMatches = map.get(match.paragraphId) ?? []; + paragraphMatches.push(match); + map.set(match.paragraphId, paragraphMatches); + } + + return map; +}; + +/** + * Transform the predictions made by AymurAI into annotations. Also append each search result as an annotation. + * @param predictions List of AymurAI predictions + * @param search Search string + * @param paragraph Paragraph to analyze + * @param searchLabel Label to assign to the search annotations + * @returns List of annotations ready to be displayed + */ +export const createAnnotationsWithSearch = ( + predictions: PredictLabel[], + searchMatches: SearchMatch[], + searchLabel: AllLabels | AllLabelsWithSufix | null, + activeSearchMatchId: string | null, +): Annotation[] => { + const matchingAnnotations = labelToAnnotation(predictions); + const searchAnnotations = getSearchAnnotations( + searchMatches, + searchLabel, + activeSearchMatchId, + ); + + return [...matchingAnnotations, ...searchAnnotations]; +}; diff --git a/frontend/src/renderer/src/components/file-annotator/generateSplits.ts b/frontend/src/renderer/src/components/file-annotator/generateSplits.ts new file mode 100644 index 00000000..24bb977a --- /dev/null +++ b/frontend/src/renderer/src/components/file-annotator/generateSplits.ts @@ -0,0 +1,119 @@ +import type { Annotation, Split } from "./types"; + +const addTrailingText = (splits: Split[], i: number, { start }: Annotation) => { + if (start > i) { + splits.push({ + end: start, + start: i, + type: "text", + }); + } +}; +const addLeadingText = (splits: Split[], i: number, { length }: string) => { + if (i < length) { + splits.push({ + end: length, + start: i, + type: "text", + }); + } +}; + +const sortTokens = (tokens: Annotation[]) => { + return tokens.sort((a, b) => a.start - b.start); +}; +const rangesOverlap = (a: Annotation, b: Annotation) => { + return a.start < b.end && b.start < a.end; +}; +const isRightConflicting = (splits: Split[], token: Annotation) => { + return (splits.at(-1)?.end ?? 0) > token.start; +}; + +const isLeftConflicting = (token: Annotation, next: Annotation | undefined) => { + if (!next) return false; + + return ( + token.type === "search" && next.type === "tag" && token.end > next.start + ); +}; + +const mergeSearchIntoTags = (tokens: Annotation[]): Annotation[] => { + const tags = tokens.filter((token) => token.type === "tag"); + const searches = tokens.filter((token) => token.type === "search"); + const overlappingSearchIds = new Set(); + + const enrichedTags = tags.map((tag) => { + const overlappingSearches = searches.filter((search) => + rangesOverlap(tag, search), + ); + if (overlappingSearches.length === 0) return tag; + + for (const search of overlappingSearches) { + if (search.searchMatchId) overlappingSearchIds.add(search.searchMatchId); + } + + const activeSearch = + overlappingSearches.find((search) => search.isActive) ?? + overlappingSearches[0]; + + return { + ...tag, + searchMatchId: activeSearch?.searchMatchId, + searchIndex: activeSearch?.searchIndex, + isActive: activeSearch?.isActive ?? false, + }; + }); + + const visibleSearches = searches.filter( + (search) => + !search.searchMatchId || !overlappingSearchIds.has(search.searchMatchId), + ); + + return [...enrichedTags, ...visibleSearches]; +}; + +/** + * Generates a list of splits from a paragraph and a list of annotations + * @param paragraph Paragraph text to split + * @param tokens List of annotations + * @returns List of splits + */ +export const generateSplits = ( + paragraph: string, + tokens: Annotation[], +): Split[] => { + if (!tokens.length) + return [ + { + start: 0, + end: paragraph.length, + type: "text", + }, + ]; + + const sortedTokens = sortTokens(mergeSearchIntoTags(tokens)); + const splits: Split[] = []; + // Represents the last index used to split + let i = 0; + + sortedTokens.forEach((token, tokenIndex) => { + // Avoid token superposition on search tokens + if ( + isRightConflicting(splits, token) || + isLeftConflicting(token, sortedTokens[tokenIndex + 1]) + ) + return; + + // Add trailing text split if necessary + addTrailingText(splits, i, token); + + splits.push(token); + // Move the cursor to the last position + i = token.end; + }); + + // Add a remaining split if the `i` has not reached the end + addLeadingText(splits, i, paragraph); + + return splits; +}; diff --git a/frontend/src/renderer/src/components/file-annotator/index.tsx b/frontend/src/renderer/src/components/file-annotator/index.tsx new file mode 100644 index 00000000..fdce8da6 --- /dev/null +++ b/frontend/src/renderer/src/components/file-annotator/index.tsx @@ -0,0 +1,269 @@ +import { + memo, + useCallback, + useEffect, + useMemo, + useRef, + useState, + useTransition, +} from "react"; + +import { SearchBar } from "./SearchBar"; + +import type { SelectOption } from "@/components/ui/select"; +import AnnotationProvider, { useAnnotation } from "@/context/Annotation"; +import { useExcludedTagsConfig } from "@/store/useLocal"; +import { css } from "@/styled/css"; +import { HStack } from "@/styled/jsx"; +import type { AllLabels, PredictLabel } from "@/types/aymurai"; +import type { DocFile, Paragraph as ParagraphType } from "@/types/file"; +import { getActiveAnonymizerLabelOptions } from "@/utils/anonymizer/labels"; +import { filterActivePredictions } from "@/utils/anonymizer/predictions"; +import LabelManager from "../anonymizer/label-manager"; +import SearchAnnotation from "../file/search-annotation"; +import TagAnnotation from "../file/tag-annotation"; +import * as S from "./FileAnnotator.styles"; +import { + type SearchMatch, + createAnnotationsWithSearch, + createSearchMatches, + predictionsToMap, + searchMatchesToMap, +} from "./annotations"; +import { generateSplits } from "./generateSplits"; + +const labelManagerWrapper = css({ + display: "flex", + h: "full", + minH: "0", + flexShrink: "0", + "&[hidden]": { + display: "none", + }, +}); + +interface ParagraphProps { + children: string; + paragraph: ParagraphType; + predictions: PredictLabel[]; + searchMatches: SearchMatch[]; + activeSearchMatchId: string | null; +} +const Paragraph = memo( + ({ + children, + paragraph, + predictions, + searchMatches, + activeSearchMatchId, + }: ParagraphProps) => { + const { label } = useAnnotation(); + + const annotations = useMemo(() => { + return createAnnotationsWithSearch( + predictions, + searchMatches, + label, + activeSearchMatchId, + ); + }, [predictions, searchMatches, label, activeSearchMatchId]); + + const splits = generateSplits(children, annotations); + + return ( + + {splits.map((s) => { + const content = children.slice(s.start, s.end); + const key = `${s.type}-${s.start}-${s.end}`; + + switch (s.type) { + case "search": + return ( + + {content} + + ); + case "tag": + return ( + + {content} + + ); + case "text": + default: + return ( + + {content} + + ); + } + })} + + ); + }, +); + +interface Props { + file: DocFile; + isAnnotable?: boolean; +} +export default function FileAnnotator({ file, isAnnotable = false }: Props) { + const [search, setSearch] = useState(""); + const [activeSearchIndex, setActiveSearchIndex] = useState( + null, + ); + const [, startTransition] = useTransition(); + const fileRef = useRef(null); + + const [label, setLabel] = useState(null); + const [labelManagerOpen, setLabelManagerOpen] = useState(isAnnotable); + + const paragraphs = file.paragraphs ?? []; + const { tags, words } = useExcludedTagsConfig(); + const activeLabelOptions = useMemo( + () => getActiveAnonymizerLabelOptions(tags), + [tags], + ); + + useEffect(() => { + if (label && !activeLabelOptions.some((option) => option.id === label)) { + setLabel(null); + } + }, [activeLabelOptions, label]); + + const filteredPredictions = useMemo( + () => filterActivePredictions(file.predictions, tags, words), + [file.predictions, tags, words], + ); + + const predictionsMap = useMemo( + () => predictionsToMap(filteredPredictions), + [filteredPredictions], + ); + + const searchMatches = useMemo( + () => createSearchMatches(paragraphs, search), + [paragraphs, search], + ); + + const searchMatchesMap = useMemo( + () => searchMatchesToMap(searchMatches), + [searchMatches], + ); + + const activeSearchMatch = + activeSearchIndex === null + ? null + : (searchMatches[activeSearchIndex] ?? null); + const activeSearchMatchId = activeSearchMatch?.id ?? null; + + useEffect(() => { + if (searchMatches.length === 0) { + setActiveSearchIndex(null); + return; + } + + setActiveSearchIndex(0); + }, [searchMatches]); + + useEffect(() => { + if (!activeSearchMatchId) return; + + const timer = window.setTimeout(() => { + const container = fileRef.current; + const element = Array.from( + container?.querySelectorAll("[data-search-match-id]") ?? [], + ).find((el) => el.getAttribute("data-search-match-id") === activeSearchMatchId); + if (!element || !container) return; + + const containerRect = container.getBoundingClientRect(); + const elementRect = element.getBoundingClientRect(); + const scrollOffset = + elementRect.top - + containerRect.top - + containerRect.height / 2 + + elementRect.height / 2; + + container.scrollBy({ + top: scrollOffset, + behavior: "smooth", + }); + }, 50); + + return () => window.clearTimeout(timer); + }, [activeSearchMatchId]); + + const selectChangeHandler = (option?: SelectOption) => { + setLabel((option?.id as AllLabels) ?? null); + }; + + const toggleManagerLabel = () => { + setLabelManagerOpen(!labelManagerOpen); + }; + + const handleSearchChange = (value: string) => { + startTransition(() => setSearch(value)); + }; + + const handleSearchNext = useCallback(() => { + setActiveSearchIndex((current) => { + if (searchMatches.length === 0) return null; + if (current === null) return 0; + return Math.min(current + 1, searchMatches.length - 1); + }); + }, [searchMatches.length]); + + const handleSearchPrevious = useCallback(() => { + setActiveSearchIndex((current) => { + if (searchMatches.length === 0) return null; + if (current === null) return 0; + return Math.max(current - 1, 0); + }); + }, [searchMatches.length]); + + const focusDocument = useCallback(() => { + fileRef.current?.focus(); + }, []); + + return ( + + + + + + {paragraphs.map((p) => ( + + {p.value} + + ))} + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/file-annotator/types.ts b/frontend/src/renderer/src/components/file-annotator/types.ts new file mode 100644 index 00000000..cf62be88 --- /dev/null +++ b/frontend/src/renderer/src/components/file-annotator/types.ts @@ -0,0 +1,39 @@ +import type { AllLabels, AllLabelsWithSufix } from "@/types/aymurai"; + +export interface BaseAnnotation { + start: number; + end: number; +} + +interface BaseLabelAnnotation extends BaseAnnotation { + paragraphId: string; + tag?: AllLabels | AllLabelsWithSufix; + canonical_entity_id?: string | null; + mentionId?: string; + searchMatchId?: string; + searchIndex?: number; + isActive?: boolean; +} + +export interface LabelAnnotation extends BaseLabelAnnotation { + type: "tag"; +} + +export interface SearchAnnotation extends BaseLabelAnnotation { + type: "search"; +} + +export interface TextAnnotation extends BaseAnnotation { + type: "text"; +} + +export type Annotation = LabelAnnotation | SearchAnnotation | TextAnnotation; +export type Split = Annotation; + +export interface Metadata { + "data-start": number; + "data-end": number; + "data-tag"?: string; + "data-search-match-id"?: string; + "data-search-active"?: string; +} diff --git a/frontend/src/renderer/src/components/file-check/ErrorText.ts b/frontend/src/renderer/src/components/file-check/ErrorText.ts new file mode 100644 index 00000000..9626281f --- /dev/null +++ b/frontend/src/renderer/src/components/file-check/ErrorText.ts @@ -0,0 +1,13 @@ +import { styled } from "@/styles"; + +const ErrorText = styled("p", { + fontStyle: "italic", + color: "$errorPrimary", + textAlign: "center", + fontSize: "$subtitleSm", + lineHeight: "$subtitleSm", + fontWeight: "$default", + whiteSpace: "pre-line", +}); + +export default ErrorText; diff --git a/frontend/src/renderer/src/components/file-check/FileCheck.styles.ts b/frontend/src/renderer/src/components/file-check/FileCheck.styles.ts new file mode 100644 index 00000000..3ff2e007 --- /dev/null +++ b/frontend/src/renderer/src/components/file-check/FileCheck.styles.ts @@ -0,0 +1,53 @@ +import { styled } from "@/styles"; + +export const Wrapper = styled("div", { + display: "flex", + flexDirection: "column", + flexWrap: "wrap", + justifyContent: "center", + alignItems: "center", + gap: "$s", + + position: "relative", + + // Settings to enable ellipsis on file name + maxWidth: 150, + "& p": { + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + width: "100%", + }, +}); + +export const Card = styled("div", { + display: "flex", + flexDirection: "column", + flexWrap: "wrap", + justifyContent: "center", + alignItems: "center", + + height: 200, + width: 150, + + borderWidth: "$sizes$xs", + borderStyle: "solid", + borderRadius: "$s", + boxShadow: "0px 0px $sizes$xs rgba(0, 0, 0, 0.1)", + + variants: { + hasError: { + true: { + bg: "$errorSecondary", + borderColor: "$errorPrimary", + }, + false: { + bg: "$bgPrimary", + borderColor: "$borderPrimary", + }, + }, + }, + defaultVariants: { + hasError: false, + }, +}); diff --git a/frontend/src/renderer/src/components/file-check/Icon.tsx b/frontend/src/renderer/src/components/file-check/Icon.tsx new file mode 100644 index 00000000..9358703e --- /dev/null +++ b/frontend/src/renderer/src/components/file-check/Icon.tsx @@ -0,0 +1,14 @@ +import { Spinner } from "@/components"; +import { CheckCircle, XCircle } from "phosphor-react"; + +import { colors } from "@/styles/tokens"; + +interface Props { + hasError: boolean; + isLoading: boolean; +} +export default function Icon({ hasError, isLoading }: Props) { + if (hasError) return ; + if (isLoading) return ; + return ; +} diff --git a/frontend/src/renderer/src/components/file-check/index.tsx b/frontend/src/renderer/src/components/file-check/index.tsx new file mode 100644 index 00000000..a6b3467f --- /dev/null +++ b/frontend/src/renderer/src/components/file-check/index.tsx @@ -0,0 +1,27 @@ +import { Text } from "@/components"; +import ErrorText from "./ErrorText"; +import { Card, Wrapper } from "./FileCheck.styles"; +import Icon from "./Icon"; + +interface Props { + fileName: string; + hasError?: boolean; + isLoading?: boolean; + errorMessage?: string; +} +export default function FileCheck({ + fileName, + hasError = false, + isLoading = false, + errorMessage = "Error de guardado\nVolvé a cargar el archivo", +}: Props) { + return ( + + + + + {fileName} + {hasError && {errorMessage}} + + ); +} diff --git a/frontend/src/renderer/src/components/file-preview/FilePreview.styles.ts b/frontend/src/renderer/src/components/file-preview/FilePreview.styles.ts new file mode 100644 index 00000000..0e968ff1 --- /dev/null +++ b/frontend/src/renderer/src/components/file-preview/FilePreview.styles.ts @@ -0,0 +1,70 @@ +import { styled } from "@/styles"; + +export const Wrapper = styled("div", { + display: "flex", + flexDirection: "column", + flexWrap: "wrap", + justifyContent: "start", + alignItems: "center", + gap: "$s", + + position: "relative", + + // Settings to enable ellipsis on file name + maxWidth: 150, + "& p": { + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + width: "100%", + }, +}); + +export const FileContainer = styled("div", { + height: 200, + width: 150, + p: "$xs", + + boxShadow: "0px 0px $sizes$xs rgba(0, 0, 0, 0.1)", + borderRadius: "$s", + b: "$sizes$xs solid $borderPrimary", + + "& p, & span, & strong, & em": { + fontFamily: "$file", + fontSize: 8, + lineHeight: "100%", + my: "1em", + }, + + // Only shows text that fits into the box + overflow: "hidden", + + variants: { + isLoading: { + true: { + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + false: {}, + }, + + error: { + true: { + borderColor: "$colors$errorPrimary", + backgroundColor: "$colors$errorSecondary", + }, + false: {}, + }, + }, +}); + +export const Paragraph = styled("p", { + fontFamily: "$file", + fontSize: "$s", + lineHeight: "$s", + margin: 0, + padding: 0, + whiteSpace: "pre-wrap", + wordBreak: "break-word", +}); diff --git a/frontend/src/renderer/src/components/file-preview/index.tsx b/frontend/src/renderer/src/components/file-preview/index.tsx new file mode 100644 index 00000000..765e6cfe --- /dev/null +++ b/frontend/src/renderer/src/components/file-preview/index.tsx @@ -0,0 +1,89 @@ +import { FileX } from "phosphor-react"; + +import { Checkbox, Spinner, Text } from "@/components"; +import { useFileDispatch } from "@/hooks"; +import type { PredictStatus } from "@/hooks/usePredict"; +import { toggleSelected } from "@/reducers/file/actions"; +import type { DocFile } from "@/types/file"; + +import { FeatureFlowEnum, parseFeatureRouteSlug } from "@/types/features"; +import { useParams } from "@tanstack/react-router"; +import { useTranslation } from "react-i18next"; +import * as S from "./FilePreview.styles"; + +interface Props { + file: DocFile; + status: PredictStatus; +} +export default function FilePreview({ file, status }: Props) { + const { feature: featureSlug } = useParams({ from: "/$feature/preview" }); + const feature = parseFeatureRouteSlug(featureSlug); + const { t } = useTranslation(); + const dispatch = useFileDispatch(); + + const isAnonymizer = feature === FeatureFlowEnum.Anonymizer; + const moreThanOneParagraph = file.paragraphs && file.paragraphs.length > 1; + const isError = status === "error"; + const isPending = status === "processing"; + + if (isError) { + return ( + + + + + + + {t("filePreview.loadError")} + + + ); + } + + if (isPending || !file.paragraphs) { + return ( + + + + + + ); + } + + return ( + + {!isAnonymizer && moreThanOneParagraph && ( + dispatch(toggleSelected(file.data.name))} + /> + )} + + + {file.paragraphs.map((p) => ( + + {p.value} + + ))} + + + + {file.data.name} + + + ); +} diff --git a/frontend/src/renderer/src/components/file-processing/FileProcessing.styles.ts b/frontend/src/renderer/src/components/file-processing/FileProcessing.styles.ts new file mode 100644 index 00000000..ec6c0d5c --- /dev/null +++ b/frontend/src/renderer/src/components/file-processing/FileProcessing.styles.ts @@ -0,0 +1,52 @@ +import { styled } from "@/styles"; + +export const BarContainer = styled("div", { + height: "$m", + width: "100%", + + bg: "$bgPrimary", + borderRadius: "$xxs", + + overflow: "hidden", +}); + +export const Bar = styled("div", { + width: "100%", + height: "100%", + + backgroundSize: "$sizes$xl $sizes$xl", + + transition: "width $s ease", + + variants: { + isCompleted: { + true: { + backgroundImage: `linear-gradient( + 135deg, + $actionDefaultAlt 37.50%, + $actionDefault 37.50%, + $actionDefault 50%, + $actionDefaultAlt 50%, + $actionDefaultAlt 87.50%, + $actionDefault 87.50%, + $actionDefault 100% + )`, + }, + false: { + backgroundImage: `linear-gradient( + 135deg, + $actionDefault 37.50%, + $actionDefaultAlt 37.50%, + $actionDefaultAlt 50%, + $actionDefault 50%, + $actionDefault 87.50%, + $actionDefaultAlt 87.50%, + $actionDefaultAlt 100% + )`, + }, + }, + }, + defaultVariants: { + isCompleted: false, + }, +}); diff --git a/frontend/src/renderer/src/components/file-processing/ProgressBar.tsx b/frontend/src/renderer/src/components/file-processing/ProgressBar.tsx new file mode 100644 index 00000000..222ee715 --- /dev/null +++ b/frontend/src/renderer/src/components/file-processing/ProgressBar.tsx @@ -0,0 +1,45 @@ +import { Label, Stack } from "@/components"; +import type { PredictStatus } from "@/hooks/usePredict"; +import { CheckCircle } from "phosphor-react"; +import { Bar, BarContainer } from "./FileProcessing.styles"; +import { ProgressLabel } from "./ProgressLabel"; + +interface Props { + fileName: string; + progress: number; + status: PredictStatus; +} +export default function ProgressBar({ fileName, progress, status }: Props) { + const getProgressText = () => { + const text = { + processing: `${progress}%`, + completed: ( + <> + Carga finalizada 100% + + ), + error: "Error de carga de archivo. Volvelo a intentar", + stopped: "Detuviste el procesamiento de este archivo", + }; + + return text[status]; + }; + + return ( + + {/* Texts */} + + + + {getProgressText()} + + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/file-processing/ProgressLabel.ts b/frontend/src/renderer/src/components/file-processing/ProgressLabel.ts new file mode 100644 index 00000000..971ccaed --- /dev/null +++ b/frontend/src/renderer/src/components/file-processing/ProgressLabel.ts @@ -0,0 +1,29 @@ +import Label from "@/components/label"; +import { styled } from "@/styles"; + +export const ProgressLabel = styled(Label, { + display: "flex", + alignItems: "center", + gap: "$xs", + + variants: { + progress: { + processing: { + color: "$textDefault", + }, + error: { + color: "$errorPrimary", + }, + stopped: { + color: "$textLighter", + fontStyle: "italic", + }, + completed: { + color: "$actionPressed", + }, + }, + }, + defaultVariants: { + progress: "processing", + }, +}); diff --git a/frontend/src/renderer/src/components/file-processing/index.tsx b/frontend/src/renderer/src/components/file-processing/index.tsx new file mode 100644 index 00000000..6768cea2 --- /dev/null +++ b/frontend/src/renderer/src/components/file-processing/index.tsx @@ -0,0 +1,111 @@ +import { ArrowsLeftRight, Stop, X } from "phosphor-react"; +import { type ChangeEventHandler, type MouseEventHandler, useRef } from "react"; + +import HiddenInput from "@/components/hidden-input"; +import Button from "@/components/ui/button"; +import { useFileDispatch } from "@/hooks"; +import type { PredictStatus } from "@/hooks/usePredict"; +import { removeFile, replaceFile } from "@/reducers/file/actions"; +import { css } from "@/styled/css"; +import { Stack } from "@/styled/jsx"; +import { useQueryClient } from "@tanstack/react-query"; +import ProgressBar from "./ProgressBar"; + +function ActionButton({ + status, + onClick, +}: { status: PredictStatus; onClick: MouseEventHandler }) { + return ( + + ); +} + +interface Props { + fileName: string; + status: PredictStatus; + progress: number; + onAbort?: () => void; +} +export default function FileProcessing({ + fileName, + status, + progress, + onAbort, +}: Props) { + const inputRef = useRef(null); + const dispatch = useFileDispatch(); + const queryClient = useQueryClient(); + + const handleOpenFinder = () => { + inputRef.current?.click(); + }; + + const handleAddedFile: ChangeEventHandler = (e) => { + const rawFiles = e.target.files; + + if (rawFiles) { + const fileList = Array.from(rawFiles); + if (fileList.length > 0) { + queryClient.removeQueries({ + queryKey: ["file-parser", fileName], + exact: false, + }); + queryClient.removeQueries({ + queryKey: ["disambiguate", fileName], + exact: false, + }); + queryClient.removeQueries({ + predicate: (query) => { + const key = query.queryKey as unknown[]; + return key[0] === "predict" && key[2] === fileName; + }, + }); + dispatch(replaceFile(fileName, fileList[0])); + } + } + }; + + const handleStop = () => { + onAbort?.(); + }; + + const remove = () => { + onAbort?.(); + dispatch(removeFile(fileName)); + }; + + return ( + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/file-stepper/FileStepper.styles.ts b/frontend/src/renderer/src/components/file-stepper/FileStepper.styles.ts new file mode 100644 index 00000000..850c59e8 --- /dev/null +++ b/frontend/src/renderer/src/components/file-stepper/FileStepper.styles.ts @@ -0,0 +1,26 @@ +import { Button as BaseButton } from "@/components"; +import { styled } from "@/styles"; + +export const CaretButton = styled(BaseButton, { + boxShadow: "4px 0px 4px rgba(0, 0, 0, 0.05)", + + // Marked as important because of Stitches hierarchy + px: "0px !important", + py: "$s !important", + + "&:disabled": { + boxShadow: "none", + color: "$textLighter", + }, +}); + +export const Carousel = styled("div", { + display: "flex", + flexDirection: "row", + gap: "$s", + alignItems: "center", + wrap: "no-wrap", + flex: 1, + + overflowX: "scroll", +}); diff --git a/frontend/src/renderer/src/components/file-stepper/Icons.tsx b/frontend/src/renderer/src/components/file-stepper/Icons.tsx new file mode 100644 index 00000000..e5907826 --- /dev/null +++ b/frontend/src/renderer/src/components/file-stepper/Icons.tsx @@ -0,0 +1,26 @@ +import { CaretLeft, CaretRight, CheckCircle } from "phosphor-react"; + +import { colors } from "@/styles/tokens"; + +function ArrowRight() { + return ; +} +function ArrowLeft() { + return ; +} + +interface Props { + status: "completed" | "focus" | "default"; +} +function Check({ status }: Props) { + return status === "completed" ? ( + + ) : null; +} + +const Icons = { + ArrowRight, + ArrowLeft, + Check, +}; +export default Icons; diff --git a/frontend/src/renderer/src/components/file-stepper/index.tsx b/frontend/src/renderer/src/components/file-stepper/index.tsx new file mode 100644 index 00000000..1053e8c4 --- /dev/null +++ b/frontend/src/renderer/src/components/file-stepper/index.tsx @@ -0,0 +1,67 @@ +import { TabName as FileName, Tab as FileStep, Stack } from "@/components"; +import { useFiles } from "@/hooks"; +import type { DocFile } from "@/types/file"; +import { CaretButton, Carousel } from "./FileStepper.styles"; +import Icons from "./Icons"; +import { canMoveLeft as checkLeft, canMoveRight as checkRight } from "./utils"; + +interface Props { + selected: number; + nextFile: () => void; + previousFile: () => void; +} +export default function FileStepper({ + selected, + nextFile, + previousFile, +}: Props) { + const files = useFiles(); + + const canMoveLeft = checkLeft(selected, files); + const canMoveRight = checkRight(selected, files); + const isLeftDisabled = selected === 0; + const isRightDisabled = selected === files.length - 1; // -1 because we are using base 0 notation + + const getStatus = (current: number, file: DocFile) => { + if (file.validated) { + return "completed"; + } + return selected === current ? "focus" : "default"; + }; + + return ( + + {/* <- */} + {canMoveLeft && ( + + + + )} + + {/* List of files */} + + {files.map((file, i) => ( + + {file.data.name} + + + ))} + + + {/* -> */} + {canMoveRight && ( + + + + )} + + ); +} diff --git a/frontend/src/renderer/src/components/file-stepper/utils.ts b/frontend/src/renderer/src/components/file-stepper/utils.ts new file mode 100644 index 00000000..ef1e2034 --- /dev/null +++ b/frontend/src/renderer/src/components/file-stepper/utils.ts @@ -0,0 +1,27 @@ +import type { DocFile } from "@/types/file"; + +/** + * Check if the stepper can move to the left + * @param cur Current `selected` index + * @param state State array of files + * @returns `true` if the stepper can move to the left (or to a previous file), `false` otherwise + */ +export function canMoveLeft(cur: number, state: DocFile[]) { + const sliced = state.slice(0, cur); + + // Check if we find any file that is not validated yet + return !!sliced.find((file) => !file.validated); +} + +/** + * Check if the stepper can move to the right + * @param cur Current `selected` index + * @param state State array of files + * @returns `true` if the stepper can move to the right (or to a next file), `false` otherwise + */ +export function canMoveRight(cur: number, state: DocFile[]) { + const sliced = state.slice(cur + 1, state.length); + + // Check if we find any file that is not validated yet + return !!sliced.find((file) => !file.validated); +} diff --git a/frontend/src/renderer/src/components/file/annotation-popover/index.tsx b/frontend/src/renderer/src/components/file/annotation-popover/index.tsx new file mode 100644 index 00000000..e8693bde --- /dev/null +++ b/frontend/src/renderer/src/components/file/annotation-popover/index.tsx @@ -0,0 +1,191 @@ +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { css } from "@/styled/css"; +import type { AllLabels } from "@/types/aymurai"; +import type { FocusEventHandler, ReactNode } from "react"; +import { useEffect, useId, useRef, useState } from "react"; +import Tagger from "./tagger"; + +const CLOSE_DELAY_MS = 120; +const OPEN_EVENT = "aymurai:annotation-popover-open"; + +const triggerFocus = css({ + "&:focus-visible": { + outline: "[2px solid token(colors.action.hover)]", + outlineOffset: "[2px]", + borderRadius: "sm", + }, +}); + +interface AnnotationPopoverProps { + children: ReactNode; + onClickOne: (label: AllLabels) => void; + onClickAll: (label: AllLabels) => void; + onDeleteOne?: () => void; + onDeleteAll?: () => void; + onHoverChange?: (hovered: boolean) => void; +} + +export default function AnnotationPopover({ + children, + onClickAll, + onClickOne, + onDeleteOne, + onDeleteAll, + onHoverChange, +}: AnnotationPopoverProps) { + const popoverId = useId(); + const [open, setOpen] = useState(false); + const closeTimer = useRef | null>(null); + const isTriggerHovered = useRef(false); + const isContentHovered = useRef(false); + const isFocusInside = useRef(false); + const isKeyboardOpen = useRef(false); + + const cancelClose = () => { + if (closeTimer.current) { + clearTimeout(closeTimer.current); + closeTimer.current = null; + } + }; + + useEffect(() => { + const closeWhenAnotherOpens = (event: Event) => { + const openedId = (event as CustomEvent).detail; + if (openedId === popoverId) return; + + isTriggerHovered.current = false; + isContentHovered.current = false; + isFocusInside.current = false; + setOpen(false); + if (closeTimer.current) { + clearTimeout(closeTimer.current); + closeTimer.current = null; + } + }; + + window.addEventListener(OPEN_EVENT, closeWhenAnotherOpens); + + return () => { + window.removeEventListener(OPEN_EVENT, closeWhenAnotherOpens); + if (closeTimer.current) clearTimeout(closeTimer.current); + }; + }, [popoverId]); + + const setInteractive = (nextOpen: boolean, announce = false) => { + setOpen(nextOpen); + onHoverChange?.(nextOpen); + if (nextOpen && announce) { + window.dispatchEvent(new CustomEvent(OPEN_EVENT, { detail: popoverId })); + } + }; + + const scheduleClose = () => { + cancelClose(); + closeTimer.current = setTimeout(() => { + if ( + !isFocusInside.current && + !isTriggerHovered.current && + !isContentHovered.current + ) { + setInteractive(false); + } + }, CLOSE_DELAY_MS); + }; + + const handleBlur: FocusEventHandler = (e) => { + if (!e.currentTarget.contains(e.relatedTarget)) { + isFocusInside.current = false; + scheduleClose(); + } + }; + + return ( + { + if (nextOpen) { + cancelClose(); + setInteractive(true, true); + return; + } + if ( + isTriggerHovered.current || + isContentHovered.current || + isFocusInside.current + ) { + scheduleClose(); + return; + } + setInteractive(false); + }} + > + { + isTriggerHovered.current = true; + cancelClose(); + setInteractive(true, true); + }} + onMouseLeave={() => { + isTriggerHovered.current = false; + scheduleClose(); + }} + onFocus={() => { + isFocusInside.current = true; + cancelClose(); + setInteractive(true, true); + }} + onBlur={() => { + isFocusInside.current = false; + scheduleClose(); + }} + onKeyDown={(e) => { + if (e.key === " " || e.key === "Enter") { + isKeyboardOpen.current = true; + } + }} + > + {children} + + { + if (!isKeyboardOpen.current) e.preventDefault(); + }} + onCloseAutoFocus={(e) => { + if (!isKeyboardOpen.current) e.preventDefault(); + isKeyboardOpen.current = false; + }} + onMouseEnter={() => { + isContentHovered.current = true; + cancelClose(); + onHoverChange?.(true); + }} + onMouseLeave={() => { + isContentHovered.current = false; + scheduleClose(); + }} + onFocus={() => { + isFocusInside.current = true; + cancelClose(); + onHoverChange?.(true); + }} + onBlur={handleBlur} + > + + + + ); +} diff --git a/frontend/src/renderer/src/components/file/annotation-popover/tagger-button.tsx b/frontend/src/renderer/src/components/file/annotation-popover/tagger-button.tsx new file mode 100644 index 00000000..29501fd0 --- /dev/null +++ b/frontend/src/renderer/src/components/file/annotation-popover/tagger-button.tsx @@ -0,0 +1,79 @@ +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { sva } from "@/styled/css"; +import { styled } from "@/styled/jsx"; + +const button = sva({ + slots: ["button", "tooltipContent"], + base: { + button: { + p: "0.5", + rounded: "[6px]", + cursor: "pointer", + flexShrink: "0", + _hover: { + bg: "action.hover", + }, + }, + tooltipContent: { + bg: "action.hover", + color: "white", + px: "1", + py: "0.5", + rounded: "sm", + }, + }, + variants: { + disabled: { + true: { + button: { + cursor: "not-allowed", + _hover: { + bg: "transparent", + }, + }, + }, + }, + }, +}); + +interface TaggerButtonProps { + tooltip: string; + children: React.ReactNode; + onClick: () => void; + disabled?: boolean; +} +export default function TaggerButton({ + children, + tooltip, + onClick, + disabled = false, +}: TaggerButtonProps) { + const classes = button({ disabled }); + return ( + + + + + + +
+ {tooltip} +
+
+
+
+ ); +} diff --git a/frontend/src/renderer/src/components/file/annotation-popover/tagger.tsx b/frontend/src/renderer/src/components/file/annotation-popover/tagger.tsx new file mode 100644 index 00000000..2218a185 --- /dev/null +++ b/frontend/src/renderer/src/components/file/annotation-popover/tagger.tsx @@ -0,0 +1,174 @@ +import { useState } from "react"; + +import { sva } from "@/styled/css"; +import { styled } from "@/styled/jsx"; +import { hstack } from "@/styled/patterns"; + +import AnonymizerLabelSelect from "@/components/anonymizer/anonymizer-label-select"; +import type { SelectOption } from "@/components/ui/select"; +import { useAnnotation } from "@/context/Annotation"; +import { useExcludedTagsConfig } from "@/store/useLocal"; +import type { AllLabels } from "@/types/aymurai"; +import { getActiveAnonymizerLabelOptions } from "@/utils/anonymizer/labels"; + +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +import TaggerButton from "./tagger-button"; + +const IMG_SIZE = 24; + +const tagger = sva({ + slots: ["container", "button", "divider", "tooltipContent"], + base: { + container: { + ...hstack.raw({ alignItems: "center", gap: "1" }), + p: "1", + bg: "action.alt-default", + rounded: "lg", + }, + button: { + cursor: "pointer", + }, + divider: { + alignSelf: "stretch", + width: "[1px]", + bg: "[#5960B0]", + + my: "1", + }, + tooltipContent: { + bg: "action.hover", + color: "white", + px: "1", + py: "0.5", + rounded: "sm", + }, + }, +}); + +interface MarkTaggerProps { + onClickOne: (label: AllLabels) => void; + onClickAll: (label: AllLabels) => void; + onDeleteOne?: () => void; + onDeleteAll?: () => void; +} +export default function Tagger({ + onClickAll, + onClickOne, + onDeleteOne, + onDeleteAll, +}: MarkTaggerProps) { + const { label: initialLabel } = useAnnotation(); + const { tags } = useExcludedTagsConfig(); + + const [label, setLabel] = useState(initialLabel); + const [isSelectOpen, setIsSelectOpen] = useState(false); + const options = getActiveAnonymizerLabelOptions(tags); + const activeLabel = + label && options.some((option) => option.id === label) ? label : null; + + const handleClickOne = () => { + if (!activeLabel) return; + onClickOne(activeLabel); + }; + const handleClickAll = () => { + if (!activeLabel) return; + onClickAll(activeLabel); + }; + + const handleLabelChange = (value: SelectOption) => { + setLabel(value.id as AllLabels); + }; + + const classes = tagger(); + return ( +
+ + + +
+ +
+
+ +
+ + Selecciona tipo de etiqueta + +
+
+
+
+
+ + Afectar una ocurrencia + +
+ + Afectar todas las ocurrencias + + {onDeleteOne && ( + <> +
+ + Eliminar esta ocurrencia + + + )} + {onDeleteAll && ( + <> +
+ + Eliminar todas las ocurrencias + + + )} +
+ ); +} diff --git a/frontend/src/renderer/src/components/file/manual-entity-resolution-dialog.tsx b/frontend/src/renderer/src/components/file/manual-entity-resolution-dialog.tsx new file mode 100644 index 00000000..ab15cfb6 --- /dev/null +++ b/frontend/src/renderer/src/components/file/manual-entity-resolution-dialog.tsx @@ -0,0 +1,227 @@ +import { useEffect, useMemo, useState } from "react"; + +import Button from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogTitle, +} from "@/components/ui/dialog"; +import Select from "@/components/ui/select"; +import { css } from "@/styled/css"; +import { Stack, styled } from "@/styled/jsx"; +import type { + ManualEntityResolution, + SimilarEntityGroupCandidate, +} from "@/utils/anonymizer/entity-similarity"; + +export interface ManualEntityResolutionRequest { + text: string; + label: string; + count: number; + candidates: SimilarEntityGroupCandidate[]; +} + +interface ManualEntityResolutionDialogProps { + request: ManualEntityResolutionRequest | null; + onClose: () => void; + onResolve: (resolution: ManualEntityResolution) => void; +} + +const candidateSummary = css({ + bg: "bg.primary", + border: "[1px solid #BCBAB8]", + rounded: "sm", + p: "2.5", + display: "grid", + gridTemplateColumns: "minmax(0, 1fr) max-content", + alignItems: "center", + columnGap: "3", + rowGap: "0.5", + minW: "0", +}); + +const candidateText = css({ + display: "grid", + gap: "0.5", + minW: "0", +}); + +const candidateToken = css({ + minW: "0", + overflowWrap: "anywhere", + wordBreak: "break-word", +}); + +const candidateReference = css({ + minW: "0", + color: "text.default", + overflowWrap: "anywhere", + wordBreak: "break-word", +}); + +const candidateMeta = css({ + display: "grid", + alignContent: "center", + justifyItems: "end", + gap: "0.5", + justifySelf: "end", + w: "[fit-content]", + maxW: "full", + px: "1.5", + py: "0.5", + bg: "[#E6E8FF]", + color: "[#3F479D]", + rounded: "xs", + whiteSpace: "nowrap", +}); + +const categoryLabel = css({ + fontSize: "[11px]", + fontWeight: "bold", + lineHeight: "[1.2]", +}); + +const scoreValue = css({ + fontSize: "[11px]", + fontWeight: "bold", + lineHeight: "[1.2]", +}); + +function getCandidateLabel(candidate: SimilarEntityGroupCandidate) { + const text = candidate.matchedText || "sin texto"; + return `${candidate.group.renderToken} - ${text} (${candidate.score}%)`; +} + +export default function ManualEntityResolutionDialog({ + request, + onClose, + onResolve, +}: ManualEntityResolutionDialogProps) { + const [selectedCanonicalId, setSelectedCanonicalId] = useState( + null, + ); + + useEffect(() => { + setSelectedCanonicalId(request?.candidates[0]?.group.canonicalId ?? null); + }, [request]); + + const candidate = useMemo(() => { + if (!request) return null; + return ( + request.candidates.find( + (item) => item.group.canonicalId === selectedCanonicalId, + ) ?? + request.candidates[0] ?? + null + ); + }, [request, selectedCanonicalId]); + + const candidateOptions = useMemo( + () => + request?.candidates.map((item) => ({ + id: item.group.canonicalId, + text: getCandidateLabel(item), + })) ?? [], + [request], + ); + + const isLabelConflict = candidate ? !candidate.labelMatches : false; + const selectedGroupText = candidate?.group.renderToken ?? ""; + const selectedGroupLabel = candidate?.group.renderBase ?? ""; + const mentionCount = request?.count ?? 0; + + return ( + { + if (!open) onClose(); + }} + > + + Resolver grupo de entidad + {request && candidate && ( + + + {isLabelConflict + ? `Encontramos texto similar en otra categoría. Seleccionaste ${request.label} para "${request.text}".` + : `Encontramos un grupo similar para "${request.text}".`} + + + {request.candidates.length > 1 && ( + + ); +} diff --git a/frontend/src/renderer/src/components/home/choose-host.tsx b/frontend/src/renderer/src/components/home/choose-host.tsx new file mode 100644 index 00000000..9ae555bb --- /dev/null +++ b/frontend/src/renderer/src/components/home/choose-host.tsx @@ -0,0 +1,66 @@ +import { Button } from "@/components"; +import { useRunLocalServer } from "@/services/aymurai"; +import { css } from "@/styled/css"; +import { Stack, styled } from "@/styled/jsx"; +import { useNavigate } from "@tanstack/react-router"; +import { HardDrives, Monitor } from "phosphor-react"; +import { useTranslation } from "react-i18next"; + +interface ChooseHostProps { + onRemoteClick: () => void; +} +export default function ChooseHost({ onRemoteClick }: ChooseHostProps) { + const navigate = useNavigate(); + const { run: runLocalServer, isRunning } = useRunLocalServer({ + onSuccess: () => + navigate({ + to: "/home/features", + }), + }); + + const { t } = useTranslation(); + + return ( + + + +

+ {t("home.host.howToConnect")} +

+ + +

+ {t("home.host.optionOr")} +

+ +
+
+
+ ); +} diff --git a/frontend/src/renderer/src/components/home/connect-to-host.tsx b/frontend/src/renderer/src/components/home/connect-to-host.tsx new file mode 100644 index 00000000..8581f422 --- /dev/null +++ b/frontend/src/renderer/src/components/home/connect-to-host.tsx @@ -0,0 +1,108 @@ +import { useConnectToHost } from "@/services/aymurai"; +import * as localStore from "@/store/useLocal"; +import { css } from "@/styled/css"; +import { Stack } from "@/styled/jsx"; +import { useNavigate } from "@tanstack/react-router"; +import { AxiosError } from "axios"; +import { ArrowLeft } from "phosphor-react"; +import { + type ChangeEventHandler, + type SubmitEventHandler, + useState, +} from "react"; +import { useTranslation } from "react-i18next"; +import { ZodError } from "zod"; +import Button from "../ui/button"; +import Input from "../ui/input"; + +const BackButton = ({ onClick }: { onClick: () => void }) => ( + +); + +interface ConnectToHostProps { + onBackClick: () => void; +} +export default function ConnectToHost({ onBackClick }: ConnectToHostProps) { + const navigate = useNavigate(); + const remoteHost = localStore.useServerHost() ?? ""; + const { setServerHost } = localStore.useServerHostActions(); + const { t } = useTranslation(); + + const [host, setHost] = useState(remoteHost); + + const { mutate: connectToHost, isPending, error, reset } = useConnectToHost(); + + const handleChange: ChangeEventHandler = (e) => { + setHost(e.target.value); + reset(); + }; + + const tryConnection: SubmitEventHandler = (e) => { + e.preventDefault(); + + connectToHost(host, { + onSuccess: () => { + setServerHost(host); + navigate({ + to: "/home/features", + }); + }, + }); + }; + + const errorMessage = (err: Error | null): string => { + console.error(err); + if (err instanceof AxiosError) { + if (err.code === "ERR_NETWORK") return t("home.host.errors.network"); + return t("home.host.errors.connection"); + } + + if (err instanceof ZodError) { + return t("home.host.errors.invalidResponse"); + } + + if (err instanceof TypeError) { + return t("home.host.errors.invalidUrl"); + } + + return t("home.host.errors.unknown"); + }; + + return ( + <> + + + +

+ {t("home.host.connectServerExplanation")} +

+ + + + + + +
+ + + ); +} diff --git a/frontend/src/renderer/src/components/home/stepper.tsx b/frontend/src/renderer/src/components/home/stepper.tsx new file mode 100644 index 00000000..2810ebab --- /dev/null +++ b/frontend/src/renderer/src/components/home/stepper.tsx @@ -0,0 +1,109 @@ +import { css, sva } from "@/styled/css"; +import { stack } from "@/styled/patterns"; +import { useTranslation } from "react-i18next"; + +const stepper = css({ + ...stack.raw({ align: "center", direction: "row" }), +}); + +const step = sva({ + slots: ["container", "circle", "text"], + base: { + container: stack.raw({ direction: "row", align: "center", gap: "2" }), + circle: { + ...stack.raw({ direction: "row", align: "center", justify: "center" }), + rounded: "full", + height: "9", + width: "9", + textStyle: "cta.md.strong", + }, + text: { + color: "text.default", + textStyle: "label.md.default", + display: "none", + visibility: "hidden", + }, + }, + variants: { + status: { + complete: { + circle: { + color: "text.onbutton-alternative", + bg: "action.alt-default", + }, + }, + pending: { + circle: { + color: "text.onbutton-default", + bg: "action.disabled", + }, + }, + in_progress: { + circle: { + color: "text.onbutton-default", + bg: "action.default", + border: "primary-alt", + }, + text: { display: "inline", visibility: "visible" }, + }, + }, + }, +}); + +type StepStatus = "complete" | "pending" | "in_progress"; +interface StepProps { + children: string; + status: StepStatus; + number: number; +} +function Step({ children, status, number }: StepProps) { + const classes = step({ status }); + // Used to hide text from visuals but expose to screen readers + const srOnly = css({ + srOnly: true, + }); + + return ( +
+ + + Paso {number}: {children} + + +
+ ); +} + +interface StepperProps { + currentStep: number; +} +export default function Stepper({ currentStep }: StepperProps) { + const { t } = useTranslation(); + + const status = (step: number): StepStatus => { + if (step === currentStep) return "in_progress"; + if (step > currentStep) return "pending"; + return "complete"; + }; + + return ( +
+ + {t("stepper.selection")} + + + {t("stepper.extraction")} + + + {t("stepper.validation")} + + + {t("stepper.finalization")} + +
+ ); +} diff --git a/frontend/src/renderer/src/components/how-it-works-modal.tsx b/frontend/src/renderer/src/components/how-it-works-modal.tsx new file mode 100644 index 00000000..92541d04 --- /dev/null +++ b/frontend/src/renderer/src/components/how-it-works-modal.tsx @@ -0,0 +1,81 @@ +import { SectionTitle } from "@/layout/section-title"; +import { css } from "@/styled/css"; +import { HStack, Stack } from "@/styled/jsx"; +import type { FeatureFlowEnum } from "@/types/features"; +import { Question, X } from "phosphor-react"; +import { useTranslation } from "react-i18next"; +import HowItWorks from "./how-it-works"; +import { Dialog, DialogClose, DialogContent, DialogTrigger } from "./ui/dialog"; +import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; + +const tooltip = css({ + padding: "2", + textStyle: "label.md.default", +}); + +const modalButton = css({ + color: "text.lighter", + + padding: "0.5", + rounded: "sm", + + cursor: "pointer", + + transition: "colors", + + "&:hover": { + bg: "action.hover", + color: "text.onbutton-alternative", + }, +}); + +const closeButton = css({ + cursor: "pointer", +}); + +const content = css({ + overflowY: "auto", + maxH: "[90vh]", + maxW: "5xl!", +}); + +interface HowItWorksModalProps { + feature: FeatureFlowEnum; +} +export default function HowItWorksModal({ feature }: HowItWorksModalProps) { + const { t } = useTranslation(); + + return ( + + + + + + + + {t("howItWorks")} + + + + + {t("howItWorks")} + + + + + } + feature={feature} + /> + + + + ); +} diff --git a/frontend/src/renderer/src/components/how-it-works.tsx b/frontend/src/renderer/src/components/how-it-works.tsx new file mode 100644 index 00000000..7c8f3876 --- /dev/null +++ b/frontend/src/renderer/src/components/how-it-works.tsx @@ -0,0 +1,110 @@ +import { SectionTitle } from "@/layout/section-title"; +import { css } from "@/styled/css"; +import { Grid, Stack, styled } from "@/styled/jsx"; +import { hstack, stack } from "@/styled/patterns"; +import { type FeatureFlowEnum, featureNamespace } from "@/types/features"; +import { useTranslation } from "react-i18next"; + +const card = css({ + ...hstack.raw({ gap: "4", alignItems: "center" }), + + bg: "bg.secondary", + border: "primary", + rounded: "sm", + + px: "4", + py: "6", + + "& > img": { + width: "[200px]", + height: "[130px]", + objectFit: "contain", + flexShrink: "0", + }, +}); + +const stepStyle = css({ + ...stack.raw({ align: "center", justify: "center" }), + + bg: "action.alt-default", + color: "text.onbutton-alternative", + rounded: "full", + textStyle: "cta.md.strong", + + width: "9", + height: "9", +}); + +interface CardProps { + img: string; + imgAlt: string; + step: number; + title: string; + subtitle: string; +} +function Card({ img, imgAlt, step, title, subtitle }: CardProps) { + return ( +
+ {imgAlt} + +

{step}

+ + {title} + {subtitle} + +
+
+ ); +} + +interface HowItWorksProps { + title?: React.ReactNode; + feature: FeatureFlowEnum; +} +export default function HowItWorks({ title, feature }: HowItWorksProps) { + const { t } = useTranslation(); + const { t: tFeature } = useTranslation(featureNamespace[feature]); + + const renderTitle = + typeof title === "string" ? ( + {title} + ) : ( + (title ?? {t("howItWorks")}) + ); + + return ( + + {renderTitle} + + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/index.ts b/frontend/src/renderer/src/components/index.ts new file mode 100644 index 00000000..15c0ea80 --- /dev/null +++ b/frontend/src/renderer/src/components/index.ts @@ -0,0 +1,23 @@ +export { default as Arrow } from "./arrow"; +export { Checkbox, CheckboxGroup } from "./checkbox"; +export { default as DecisionTabs } from "./decision-tabs"; +export { default as FileAnnotator } from "./file-annotator"; +export { default as FileCheck } from "./file-check"; +export { default as FilePreview } from "./file-preview"; +export { default as FileProcessing } from "./file-processing"; +export { default as FileStepper } from "./file-stepper"; +export { default as Grid } from "./grid"; +export { default as Label } from "./label"; +export { OnboardingGrid } from "./onboarding-grid"; +export { Radio, RadioGroup } from "./radio"; +export { default as Spinner } from "./spinner"; +export { default as Stack } from "./stack"; +export { default as Subtitle } from "./subtitle"; +export { Tab, TabName } from "./tabs"; +export { default as Text } from "./text"; +export { default as ThemeProvider } from "./theme-provider"; +export { default as Title } from "./title"; +export { default as Button } from "./ui/button"; +export { default as Input } from "./uncontrolled-input"; +export { ValidateDataset } from "./validate-dataset"; +export { default as ValidationForm } from "./validation-form"; diff --git a/frontend/src/renderer/src/components/label/index.ts b/frontend/src/renderer/src/components/label/index.ts new file mode 100644 index 00000000..08531a91 --- /dev/null +++ b/frontend/src/renderer/src/components/label/index.ts @@ -0,0 +1,42 @@ +import { styled } from "@/styles"; + +/** + * @deprecated use `textStyles` from PandaCSS instead. + */ +const Label = styled("label", { + variants: { + size: { + m: { + fontSize: "$labelMd", + lineHeight: "$labelMd", + }, + s: { + fontSize: "$labelSm", + lineHeight: "$labelSm", + }, + }, + weight: { + default: { + fontWeight: "$default", + }, + strong: { + fontWeight: "$strong", + }, + }, + status: { + default: { + color: "$textLighter", + }, + error: { + color: "$errorPrimary", + }, + }, + }, + defaultVariants: { + size: "m", + weight: "default", + status: "default", + }, +}); + +export default Label; diff --git a/frontend/src/renderer/src/components/layout/footer.tsx b/frontend/src/renderer/src/components/layout/footer.tsx new file mode 100644 index 00000000..6de643c4 --- /dev/null +++ b/frontend/src/renderer/src/components/layout/footer.tsx @@ -0,0 +1,35 @@ +import { css, cx } from "@/styled/css"; +import BuiltBy from "../brand/built-by"; + +const content = css({ + display: "flex", + flexDirection: "row", + alignItems: "center", + + borderTop: "primary", + px: "12", + py: "6", +}); +const childrenContainer = css({ + flex: "1", + display: "flex", + justifyContent: "flex-end", +}); + +interface FooterProps { + withBuiltBy?: boolean; + children?: React.ReactNode; + className?: string; +} +export default function Footer({ + withBuiltBy, + children, + className, +}: FooterProps) { + return ( +
+ {withBuiltBy && } +
{children}
+
+ ); +} diff --git a/frontend/src/renderer/src/components/layout/header.tsx b/frontend/src/renderer/src/components/layout/header.tsx new file mode 100644 index 00000000..f1b1d0a4 --- /dev/null +++ b/frontend/src/renderer/src/components/layout/header.tsx @@ -0,0 +1,68 @@ +import { useTutorialSeen } from "@/store/useLocal"; +import { css } from "@/styled/css"; +import { Divider, HStack, Stack, styled } from "@/styled/jsx"; +import type { FeatureFlowEnum } from "@/types/features"; +import { Link } from "@tanstack/react-router"; +import FeaturesMenu from "../features-menu"; +import HowItWorksModal from "../how-it-works-modal"; + +const header = css({ + position: "relative", + width: "full", + height: "24", + py: "6", + px: "12", + bg: "bg.secondary", + borderBottom: "[1px solid #BCBAB8]", + display: "flex", + alignItems: "center", + justifyContent: "space-between", +}); + +const centerSlot = css({ + position: "absolute", + left: "[50%]", + transform: "translateX(-50%)", +}); + +interface HeaderProps { + title?: string; + center?: React.ReactNode; + feature?: FeatureFlowEnum; + right?: React.ReactNode; +} + +export default function Header({ title, center, feature, right }: HeaderProps) { + const tutorialSeen = useTutorialSeen(feature!); + + const img = title + ? `${import.meta.env.BASE_URL}brand/aymurai-iso-darkpurple.svg` + : `${import.meta.env.BASE_URL}brand/aymurai-hor-darkpurple.svg`; + + return ( +
+ + + AymurAI logo + {title && ( + <> + + {title} + + )} + + + {center &&
{center}
} + + {tutorialSeen && feature && } + {right} + + +
+ ); +} diff --git a/frontend/src/renderer/src/components/layout/home-button.tsx b/frontend/src/renderer/src/components/layout/home-button.tsx new file mode 100644 index 00000000..777409d5 --- /dev/null +++ b/frontend/src/renderer/src/components/layout/home-button.tsx @@ -0,0 +1,16 @@ +import { css } from "@/styled/css"; +import { House } from "phosphor-react"; +import Link from "../ui/link"; + +export default function HomeButton() { + return ( + + + + ); +} diff --git a/frontend/src/renderer/src/components/layout/main-content.tsx b/frontend/src/renderer/src/components/layout/main-content.tsx new file mode 100644 index 00000000..139fd857 --- /dev/null +++ b/frontend/src/renderer/src/components/layout/main-content.tsx @@ -0,0 +1,48 @@ +import { cva, cx } from "@/styled/css"; +import { stack } from "@/styled/patterns"; + +const content = cva({ + base: { + flex: "1", + minH: "0", + + width: "full", + overflowY: "auto", + + bg: "bg.primary", + }, + variants: { + full: { + true: {}, + false: { + ...stack.raw({ align: "center", gap: "0" }), + + pt: { base: "6", xl: "16" }, + px: "8", + + "& > div.spacing": { + width: "full", + maxWidth: "5xl", + }, + }, + }, + }, + defaultVariants: { + full: false, + }, +}); + +interface MainContentProps { + children: React.ReactNode; + full?: boolean; +} +export default function MainContent({ + children, + full = false, +}: MainContentProps) { + return ( +
+
{children}
+
+ ); +} diff --git a/frontend/src/renderer/src/components/onboarding-grid/index.ts b/frontend/src/renderer/src/components/onboarding-grid/index.ts new file mode 100644 index 00000000..ecf7ff3a --- /dev/null +++ b/frontend/src/renderer/src/components/onboarding-grid/index.ts @@ -0,0 +1,15 @@ +import { styled } from "@/styles"; + +export const OnboardingGrid = styled("div", { + display: "grid", + justifyItems: "center", + gridTemplateColumns: "repeat(1, minmax(200px, 1fr))", + gap: "$xl 0", + + "@md": { + gridTemplateColumns: "repeat(4, minmax(160px, 1fr))", + }, + "@xl": { + gridTemplateColumns: "repeat(4, minmax(200px, 1fr))", + }, +}); diff --git a/frontend/src/renderer/src/components/radio/index.ts b/frontend/src/renderer/src/components/radio/index.ts new file mode 100644 index 00000000..8e8c2681 --- /dev/null +++ b/frontend/src/renderer/src/components/radio/index.ts @@ -0,0 +1,4 @@ +import Radio from "./radio"; +import RadioGroup from "./radio-group"; + +export { RadioGroup, Radio }; diff --git a/frontend/src/renderer/src/components/radio/radio-group/RadioGroup.styles.ts b/frontend/src/renderer/src/components/radio/radio-group/RadioGroup.styles.ts new file mode 100644 index 00000000..f727702b --- /dev/null +++ b/frontend/src/renderer/src/components/radio/radio-group/RadioGroup.styles.ts @@ -0,0 +1,30 @@ +import { styled } from "@/styles"; + +export const Legend = styled("legend", { + fontWeight: "$default", + fontSize: "$subtitleSm", + lineHeight: "$subtitleSm", + color: "$textLighter", + + mb: "$xs", +}); + +export const Group = styled("fieldset", { + display: "flex", + gap: "$m", + flexWrap: "wrap", + + variants: { + direction: { + horizontal: { + flexDirection: "row", + }, + vertical: { + flexDirection: "column", + }, + }, + }, + defaultVariants: { + direction: "horizontal", + }, +}); diff --git a/frontend/src/renderer/src/components/radio/radio-group/index.tsx b/frontend/src/renderer/src/components/radio/radio-group/index.tsx new file mode 100644 index 00000000..1f9e0066 --- /dev/null +++ b/frontend/src/renderer/src/components/radio/radio-group/index.tsx @@ -0,0 +1,39 @@ +import { + Children, + type ReactElement, + type ReactNode, + cloneElement, + isValidElement, +} from "react"; + +import type { Props as RadioProps } from "../radio"; +import { Group, Legend } from "./RadioGroup.styles"; + +interface Props { + children: ReactNode; + label?: string; + name: string; + direction?: "horizontal" | "vertical"; +} +export default function RadioGroup({ + label, + name, + direction = "horizontal", + children, +}: Props) { + // Inject the name to the children + const radiosWithName = Children.map(children, (child) => { + if (isValidElement(child)) { + return cloneElement(child as ReactElement, { + name, + }); + } + }); + + return ( + + {label && {label}} + {radiosWithName} + + ); +} diff --git a/frontend/src/renderer/src/components/radio/radio/Radio.styles.ts b/frontend/src/renderer/src/components/radio/radio/Radio.styles.ts new file mode 100644 index 00000000..874e81b6 --- /dev/null +++ b/frontend/src/renderer/src/components/radio/radio/Radio.styles.ts @@ -0,0 +1,143 @@ +import { styled } from "@/styles"; + +export const Input = styled("input", { + position: "absolute", + top: 0, + left: 0, + + opacity: 0, +}); + +export const Radio = styled("div", { + display: "flex", + alignItems: "center", + justifyContent: "center", + + width: 18, + height: 18, + p: 1, + + borderRadius: "100%", + boxSizing: "border-box", +}); + +export const Wrapper = styled("label", { + // Unchecked + $$radio_bg_default: "$colors$white", + $$radio_border_default: "$sizes$xxs solid $colors$actionDefaultAlt", + // hover + $$radio_bgHover_default: "$colors$white", + $$radio_borderHover_default: "1px solid $colors$borderPrimary", + // disabled + $$radio_bgDisabled_default: "$colors$actionDisabled", + $$radio_borderDisabled_default: "$sizes$xxs solid $colors$borderPrimary", + // focus + $$radio_bgFocus_default: "$colors$white", + $$radio_borderFocus_default: "1px solid $colors$borderPrimary", + + // Checked + $$radio_bg_checked: "$colors$actionDefaultAlt", + $$radio_border_checked: "5px solid $colors$actionDefaultAlt", + // hover + $$radio_bgHover_checked: "$colors$actionHover", + $$radio_borderHover_checked: "5px solid $colors$actionHover", + // disabled + $$radio_bgDisabled_checked: "$colors$actionDisabled", + $$radio_borderDisabled_checked: "none", + // focus + $$radio_bgFocus_checked: "$colors$actionDefaultAlt", + $$radio_borderFocus_checked: "5px solid $colors$actionDefaultAlt", + + // Focus + $$radio_outlineFocus_noText: "3px solid $colors$borderPrimaryAlt", + $$radio_outlineFocus_Text: "3px solid $colors$borderPrimary", + + position: "relative", + + display: "flex", + flexDirection: "row", + alignItems: "center", + gap: "$s", + + transitionDuration: "$transitions$s", + transitionProperty: "outline, border-radius, border", + transitionTimingFunction: "ease", + + variants: { + hasText: { + true: { + p: "$s", + "&:focus-within": { + outline: "$$radio_outlineFocus_Text", + borderRadius: "3px", + }, + }, + false: { + "&:focus-within": { + outline: "$$radio_outlineFocus_noText", + borderRadius: "100%", + }, + // Focus for radio + [`& > input:checked:focus-visible + ${Radio}`]: { + bg: "$$radio_bgFocus_checked", + b: "$$radio_borderFocus_checked", + }, + [`& > input:focus-visible + ${Radio}`]: { + bg: "$$radio_bgFocus_default", + b: "$$radio_borderFocus_default", + }, + }, + }, + isDisabled: { + true: { + "&, & > *": { + cursor: "not-allowed", + }, + }, + false: { + "&, & > *": { + cursor: "pointer", + }, + }, + }, + }, + defaultVariants: { + hasText: "false", + isDisabled: "false", + }, + + // Radio styles + [`& > ${Radio}`]: { + b: "$$radio_border_default", + bg: "$$radio_bg_default", + }, + [`& > input:checked + ${Radio}`]: { + bg: "$$radio_bg_checked", + border: "$$radio_border_checked", + }, + + // Hide SVG when unchecked + [`& > input:not(:checked) + ${Radio} svg`]: { + display: "none", + }, + + // Hover + [`&:hover > ${Radio}`]: { + b: "$$radio_borderHover_default", + bg: "$$radio_bgHover_default", + }, + [`&:hover > input:checked + ${Radio}`]: { + b: "$$radio_borderHover_checked", + bg: "$$radio_bgHover_checked", + }, + + // Disabled + [`& > input:disabled + ${Radio}`]: { + bg: "$$radio_bgDisabled_default", + b: "$$radio_borderDisabled_default", + }, + [`& > input:checked:disabled + ${Radio}`]: { + bg: "$$radio_bgDisabled_checked", + b: "$$radio_borderDisabled_checked", + }, +}); diff --git a/frontend/src/renderer/src/components/radio/radio/index.tsx b/frontend/src/renderer/src/components/radio/radio/index.tsx new file mode 100644 index 00000000..57268d14 --- /dev/null +++ b/frontend/src/renderer/src/components/radio/radio/index.tsx @@ -0,0 +1,65 @@ +import { Circle } from "phosphor-react"; +import { + type ChangeEventHandler, + type ReactNode, + forwardRef, + useImperativeHandle, + useRef, +} from "react"; + +import type { CSS } from "@/styles"; +import { colors } from "@/styles/tokens"; +import { Input, Radio as StyledRadio, Wrapper } from "./Radio.styles"; + +export interface Props { + children?: ReactNode; + name?: string; + checked?: boolean; + disabled?: boolean; + onChange?: (value: boolean) => void; + css?: CSS; +} +export default forwardRef<{ value: boolean }, Props>(function Radio( + { name, checked = false, disabled = false, onChange, css, children }, + ref, +) { + const inputRef = useRef(null); + + const hasText = !!children; + + // Only exposes `value` object to the parent component + useImperativeHandle( + ref, + () => { + return { + value: inputRef.current?.checked ?? false, + }; + }, + [], + ); + + const handleToggle: ChangeEventHandler = (e) => { + onChange?.(e.target.checked); + }; + + const iconColor = disabled + ? colors.textOnButtonDisabled + : colors.textOnButtonAlternative; + + return ( + + + + + + {children} + + ); +}); diff --git a/frontend/src/renderer/src/components/spinner/index.tsx b/frontend/src/renderer/src/components/spinner/index.tsx new file mode 100644 index 00000000..78d6eed5 --- /dev/null +++ b/frontend/src/renderer/src/components/spinner/index.tsx @@ -0,0 +1,44 @@ +import { keyframes, styled } from "@/styles"; + +const spin = keyframes({ + "0%": { transform: "rotate(0deg)" }, + "100%": { transform: "rotate(360deg)" }, +}); + +const SVG = styled("svg", { + animation: `${spin} 1s linear infinite`, +}); + +export default function Spinner() { + return ( + + + + + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/stack/index.ts b/frontend/src/renderer/src/components/stack/index.ts new file mode 100644 index 00000000..d6fae295 --- /dev/null +++ b/frontend/src/renderer/src/components/stack/index.ts @@ -0,0 +1,66 @@ +import { styled } from "@/styles"; + +/** + * @deprecated Use `Stack` from `@/styled/jsx` instead. + */ +const Stack = styled("div", { + display: "flex", + + variants: { + direction: { + row: { flexDirection: "row" }, + column: { flexDirection: "column" }, + "row-reverse": { flexDirection: "row-reverse" }, + "column-reverse": { flexDirection: "column-reverse" }, + }, + + wrap: { + wrap: { flexWrap: "wrap" }, + nowrap: { flexWrap: "nowrap" }, + "wrap-reverse": { flexWrap: "wrap-reverse" }, + }, + + justify: { + start: { justifyContent: "start" }, + end: { justifyContent: "end" }, + center: { justifyContent: "center" }, + "space-between": { justifyContent: "space-between" }, + "space-around": { justifyContent: "space-around" }, + "space-evenly": { justifyContent: "space-evenly" }, + }, + + align: { + start: { alignItems: "start" }, + end: { alignItems: "end" }, + center: { alignItems: "center" }, + stretch: { alignItems: "stretch" }, + baseline: { alignItems: "baseline" }, + }, + + spacing: { + none: { gap: 0 }, + xxs: { gap: "$xxs" }, + xs: { gap: "$xs" }, + s: { gap: "$s" }, + m: { gap: "$m" }, + l: { gap: "$l" }, + xl: { gap: "$xl" }, + }, + textColor: { + default: { + color: "$textDefault", + }, + }, + }, + + defaultVariants: { + direction: "row", + wrap: "wrap", + justify: "start", + align: "start", + spacing: "s", + textColor: "default", + }, +}); + +export default Stack; diff --git a/frontend/src/renderer/src/components/subtitle/index.ts b/frontend/src/renderer/src/components/subtitle/index.ts new file mode 100644 index 00000000..acdf6e5e --- /dev/null +++ b/frontend/src/renderer/src/components/subtitle/index.ts @@ -0,0 +1,39 @@ +import { styled } from "@/styles"; + +/** + * @deprecated Use `textStyle` from PandaCSS instead. + */ +const Subtitle = styled("p", { + variants: { + size: { + m: { + fontSize: "$subtitleMd", + lineHeight: "$subtitleMd", + }, + s: { + fontSize: "$subtitleSm", + lineHeight: "$subtitleSm", + }, + }, + weight: { + default: { + fontWeight: "$default", + }, + strong: { + fontWeight: "$strong", + }, + }, + textColor: { + default: { + color: "$textDefault", + }, + }, + }, + defaultVariants: { + size: "m", + weight: "default", + textColor: "default", + }, +}); + +export default Subtitle; diff --git a/frontend/src/renderer/src/components/tabs/index.ts b/frontend/src/renderer/src/components/tabs/index.ts new file mode 100644 index 00000000..320a5514 --- /dev/null +++ b/frontend/src/renderer/src/components/tabs/index.ts @@ -0,0 +1,56 @@ +import { styled } from "@/styles"; + +export const Tab = styled("div", { + borderRadius: "$xxs", + boxSizing: "border-box", + + display: "flex", + flexDirection: "row", + gap: "$s", + alignItems: "center", + + p: "$m", + + fontSize: 16, + + variants: { + status: { + completed: { + "& label, & span": { + color: "$textOnButtonAlternative", + }, + bg: "$actionPressed", + b: "none", + }, + focus: { + "& label, & span": { + color: "$textOnButtonDefault", + }, + bg: "$actionFocus", + boxShadow: "2px 2px 10px rgba(17, 0, 65, 0.25)", + b: "1px solid $borderPrimaryAlt", + }, + default: { + "& label, & span": { + color: "$textOnButtonDefault", + }, + bg: "$primaryAlt", + b: "none", + }, + }, + }, + defaultVariants: { + status: "default", + }, +}); + +export const TabName = styled("label", { + textOverflow: "ellipsis", + overflow: "hidden", + whiteSpace: "nowrap", + + // width: 200, + + fontSize: "$labelMd", + lineHeight: "$labelMd", +}); diff --git a/frontend/src/renderer/src/components/text/index.ts b/frontend/src/renderer/src/components/text/index.ts new file mode 100644 index 00000000..c3e12968 --- /dev/null +++ b/frontend/src/renderer/src/components/text/index.ts @@ -0,0 +1,40 @@ +import { styled } from "@/styles"; + +const Text = styled("p", { + variants: { + size: { + m: { + fontSize: "$paragraphsMd", + lineHeight: "$paragraphsMd", + }, + s: { + fontSize: "$paragraphsSm", + lineHeight: "$paragraphsSm", + }, + xs: { + fontSize: "$paragraphsXsm", + lineHeight: "$paragraphsXsm", + }, + }, + weight: { + default: { + fontWeight: "$default", + }, + strong: { + fontWeight: "$strong", + }, + }, + textColor: { + default: { + color: "$textDefault", + }, + }, + }, + defaultVariants: { + size: "m", + weight: "default", + textColor: "default", + }, +}); + +export default Text; diff --git a/frontend/src/renderer/src/components/theme-provider/ThemeProvider.types.ts b/frontend/src/renderer/src/components/theme-provider/ThemeProvider.types.ts new file mode 100644 index 00000000..6186f91e --- /dev/null +++ b/frontend/src/renderer/src/components/theme-provider/ThemeProvider.types.ts @@ -0,0 +1,3 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export interface Props extends ComponentPropsWithoutRef<"div"> {} diff --git a/frontend/src/renderer/src/components/theme-provider/index.tsx b/frontend/src/renderer/src/components/theme-provider/index.tsx new file mode 100644 index 00000000..50b30fb2 --- /dev/null +++ b/frontend/src/renderer/src/components/theme-provider/index.tsx @@ -0,0 +1,6 @@ +import type { Props } from "./ThemeProvider.types"; + +export default function ThemeProvider({ children }: Props) { + // globalStyles(); + return
{children}
; +} diff --git a/frontend/src/renderer/src/components/title/index.ts b/frontend/src/renderer/src/components/title/index.ts new file mode 100644 index 00000000..668b6ec4 --- /dev/null +++ b/frontend/src/renderer/src/components/title/index.ts @@ -0,0 +1,39 @@ +import { styled } from "@/styles"; + +const Title = styled("h1", { + variants: { + size: { + default: { + fontSize: "$titleMd", + lineHeight: "$titleMd", + }, + main: { + fontSize: "$titleMain", + lineHeight: "$titleMain", + }, + }, + weight: { + default: { + fontWeight: "$default", + }, + strong: { + fontWeight: "$strong", + }, + heavy: { + fontWeight: "$heavy", + }, + }, + textColor: { + default: { + color: "$textDefault", + }, + }, + }, + defaultVariants: { + weight: "default", + size: "default", + textColor: "default", + }, +}); + +export default Title; diff --git a/frontend/src/renderer/src/components/ui/back-button.tsx b/frontend/src/renderer/src/components/ui/back-button.tsx new file mode 100644 index 00000000..c70c9070 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/back-button.tsx @@ -0,0 +1,17 @@ +import { css, cx } from "@/styled/css"; +import { createLink } from "@tanstack/react-router"; +import { ArrowLeft } from "phosphor-react"; + +const link = css({ + cursor: "pointer", +}); + +const BackButton = createLink( + ({ className, children: _, ref, ...props }: React.AnchorHTMLAttributes & { ref?: React.Ref }) => ( + + + + ), +); + +export default BackButton; diff --git a/frontend/src/renderer/src/components/ui/button.tsx b/frontend/src/renderer/src/components/ui/button.tsx new file mode 100644 index 00000000..5d015f81 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/button.tsx @@ -0,0 +1,150 @@ +import { type RecipeVariantProps, css, cva, cx } from "@/styled/css"; +import { CircleNotch } from "phosphor-react"; +import type { ButtonHTMLAttributes } from "react"; + +export const button = cva({ + base: { + display: "flex", + flexDir: "row", + gap: "1", // 4px + justifyContent: "center", + alignItems: "center", + + transitionProperty: "[background-color, color, box-shadow]", + transitionDuration: "slow", // 300ms + transitionTimingFunction: "default", + + border: "none", + + textStyle: "cta.md.strong", + + cursor: "pointer", + + "&:disabled": { + cursor: "not-allowed", + }, + }, + variants: { + variant: { + primary: { + bg: "action.default", + color: "text.onbutton-default", + + "&:hover:not(:disabled)": { + bg: "action.hover", + color: "text.onbutton-alternative", + }, + + "&:active:not(:disabled)": { + bg: "action.pressed", + color: "text.onbutton-alternative", + }, + + "&:focus:not(:disabled)": { + bg: "action.focus", + outline: "primary-alt", + outlineWidth: "[2px]", + boxShadow: "[0px 0px 10px rgba(17, 0, 65, 0.2)]", + }, + + "&:disabled": { + bg: "action.disabled", + color: "text.onbutton-default", + }, + }, + secondary: { + color: "text.onbutton-default", + bg: "bg.secondary", + + borderWidth: "[2px]", + borderStyle: "solid", + borderColor: "action.alt-default", + + "&:hover:not(:disabled)": { + color: "text.onbutton-default", + bg: "bg.secondary", + borderColor: "action.hover", + }, + + "&:active:not(:disabled)": { + color: "text.onbutton-alternative", + bg: "action.pressed", + borderColor: "action.pressed", + }, + + "&:focus:not(:disabled)": { + boxShadow: "[0px 0px 10px rgba(17, 0, 65, 0.2)]", + outline: "primary-alt", + outlineWidth: "[2px]", + bg: "bg.secondary", + }, + + "&:disabled": { + color: "text.onbutton-disabled", + bg: "bg.secondary", + borderColor: "action.disabled", + }, + }, + tertiary: { + // TODO: not implemented + }, + none: { + padding: "0", + bg: "[inherit]", + }, + }, + size: { + md: { height: "12", padding: "4", rounded: "sm" }, + sm: { height: "9", py: "2", px: "4", rounded: "sm" }, + "icon-md": { + height: "12", + width: "12", + padding: "3", + rounded: "md", + }, + "icon-sm": { height: "9", width: "9", padding: "2", rounded: "md" }, + }, + checked: { + true: { + bg: "action.pressed !important", + color: "text.onbutton-alternative !important", + }, + false: {}, + }, + }, + defaultVariants: { + size: "md", + variant: "primary", + checked: false, + }, +}); + +type ButtonProps = RecipeVariantProps & + ButtonHTMLAttributes & { + isLoading?: boolean; + }; + +export default function Button({ + size, + variant, + isLoading, + disabled, + children, + className, + checked, + ...props +}: ButtonProps) { + return ( + + ); +} diff --git a/frontend/src/renderer/src/components/ui/callout.tsx b/frontend/src/renderer/src/components/ui/callout.tsx new file mode 100644 index 00000000..d215abe2 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/callout.tsx @@ -0,0 +1,106 @@ +import { cva, cx } from "@/styled/css"; +import { styled } from "@/styled/jsx"; +import { hstack } from "@/styled/patterns"; +import type { Icon } from "phosphor-react"; +import { Bell, X } from "phosphor-react"; +import type { HTMLAttributes } from "react"; + +const calloutRecipe = cva({ + base: { + ...hstack.raw({ + alignItems: "center", + gap: "3", + }), + + px: "4", + py: "3", + + rounded: "sm", + borderWidth: "[1px]", + borderStyle: "solid", + + width: "full", + + textStyle: "paragraph.sm.default", + }, + variants: { + variant: { + error: { + bg: "system.error-secondary", + borderColor: "system.error", + color: "system.error", + }, + warning: { + bg: "system.warning-secondary", + borderColor: "system.warning", + color: "system.warning", + }, + success: { + bg: "system.success-secondary", + borderColor: "system.success", + color: "system.success", + }, + info: { + bg: "system.info-secondary", + borderColor: "system.info", + color: "text.default", + }, + }, + noBorder: { + true: { + borderWidth: "0", + }, + }, + }, + defaultVariants: { + variant: "info", + noBorder: false, + }, +}); + +export type CalloutVariant = "error" | "warning" | "success" | "info"; + +interface CalloutProps extends HTMLAttributes { + message: string; + variant?: CalloutVariant; + noBorder?: boolean; + onDismiss?: () => void; + icon?: Icon; +} + +function Callout({ + message, + variant = "info", + noBorder = false, + onDismiss, + icon: IconComponent = Bell, + className, + ...props +}: CalloutProps) { + return ( +
+
+ ); +} + +export default Callout; diff --git a/frontend/src/renderer/src/components/ui/card.tsx b/frontend/src/renderer/src/components/ui/card.tsx new file mode 100644 index 00000000..bc2b77e5 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import { cva, cx } from "@/styled/css"; + +const styles = cva({ + base: { + transitionProperty: "[border, box-shadow]", + transitionTimingFunction: "default", + transitionDuration: "normal", + }, + variants: { + size: { + lg: { + p: "8", + rounded: "sm", + }, + sm: { + rounded: "lg", + p: "4", + }, + }, + disabled: { + true: { + border: "primary", + bg: "bg.primary", + color: "text.lighter", + }, + false: { + border: "primary", + bg: "bg.secondary", + }, + }, + clickable: { + true: {}, + false: {}, + }, + }, + compoundVariants: [ + { + clickable: true, + disabled: false, + css: { + cursor: "pointer", + "&:hover": { + border: "primary-alt", + boxShadow: "[0px 0px 15px 0px #3F479D66]", + }, + }, + }, + { + clickable: true, + disabled: true, + css: { + cursor: "not-allowed", + }, + }, + ], + defaultVariants: { + disabled: false, + clickable: false, + size: "lg", + }, +}); + +interface CardProps { + children?: React.ReactNode; + disabled?: boolean; + size?: "lg" | "sm"; + clickable?: boolean; + className?: string; +} +export default function Card({ + disabled = false, + size = "lg", + clickable = false, + children, + className, +}: CardProps) { + const classes = styles({ size, disabled, clickable }); + return
{children}
; +} diff --git a/frontend/src/renderer/src/components/ui/dialog.tsx b/frontend/src/renderer/src/components/ui/dialog.tsx new file mode 100644 index 00000000..2cb6a475 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/dialog.tsx @@ -0,0 +1,115 @@ +import { css, cx } from "@/styled/css"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import type { ComponentPropsWithoutRef, HTMLAttributes } from "react"; + +const Dialog = DialogPrimitive.Root; +const DialogTrigger = DialogPrimitive.Trigger; +const DialogClose = DialogPrimitive.Close; +const DialogTitle = DialogPrimitive.Title; +const DialogDescription = DialogPrimitive.Description; + +const overlayStyles = css({ + position: "fixed", + inset: "[0]", + zIndex: 50, + bg: "[rgba(0, 0, 0, 0.5)]", + + "&[data-state='open']": { + animation: "fadeIn", + }, + "&[data-state='closed']": { + animation: "fadeOut", + }, +}); + +const contentStyles = css({ + position: "fixed", + inset: "[0]", + margin: "auto", + zIndex: 50, + + bg: "bg.secondary", + rounded: "sm", + p: "6", + boxShadow: "[0px 4px 8px rgba(0, 0, 0, 0.1)]", + + width: "[90vw]", + minWidth: "[300px]", + maxWidth: "[80%]", + h: "[fit-content]", + + "&[data-state='open']": { + animation: "fadeIn", + }, + "&[data-state='closed']": { + animation: "fadeOut", + }, +}); + +const headerStyles = css({ + display: "flex", + justifyContent: "space-between", + alignItems: "center", + mb: "4", +}); + +const footerStyles = css({ + display: "flex", + gap: "2", + justifyContent: "flex-end", + alignItems: "center", + mt: "8", +}); + +function DialogOverlay({ + className, + ...props +}: ComponentPropsWithoutRef) { + return ( + + ); +} + +function DialogContent({ + className, + container, + children, + ...props +}: ComponentPropsWithoutRef & { + container?: HTMLElement; +}) { + return ( + + + + {children} + + + ); +} + +function DialogHeader({ className, ...props }: HTMLAttributes) { + return
; +} + +function DialogFooter({ className, ...props }: HTMLAttributes) { + return
; +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogTitle, + DialogTrigger, +}; diff --git a/frontend/src/renderer/src/components/ui/input.tsx b/frontend/src/renderer/src/components/ui/input.tsx new file mode 100644 index 00000000..674a1efd --- /dev/null +++ b/frontend/src/renderer/src/components/ui/input.tsx @@ -0,0 +1,207 @@ +import { WarningCircle } from "phosphor-react"; +import { useId } from "react"; + +import Suggestion from "@/components/ui/suggestion"; +import { cva, sva } from "@/styled/css"; +import { hstack, stack } from "@/styled/patterns"; + +const input = sva({ + slots: ["container", "inputBox", "input", "label", "errorMessage", "helper"], + base: { + container: stack.raw({ gap: "1", width: "full" }), + inputBox: { + ...hstack.raw({ alignItems: "center", gap: "1" }), + + rounded: "sm", + border: "primary", + + "&:focus-within": { + outline: "none", + boxShadow: "[0px 2px 2px rgba(0, 0, 0, 0.16)]", + }, + }, + input: { + textStyle: "label.md.default", + border: "none", + outline: "none", + flex: "[1]", + + "&::placeholder": { + color: "text.lighter", + }, + }, + label: { textStyle: "label.sm.default", color: "text.lighter" }, + errorMessage: { + ...hstack.raw({ gap: "1" }), + textStyle: "label.sm.default", + color: "system.error", + }, + helper: { + textStyle: "label.sm.default", + color: "text.lighter", + }, + }, + variants: { + size: { + md: { + input: { p: "3" }, + }, + sm: { + input: { px: "3", py: "1" }, + }, + }, + disabled: { + true: { + inputBox: { + bg: "bg.primary", + border: "primary", + color: "text.lighter", + cursor: "not-allowed", + }, + input: { + cursor: "not-allowed", + }, + }, + false: { + inputBox: { + bg: "white", + }, + }, + }, + error: { + true: { + container: { + color: "system.error", + }, + inputBox: { + border: "error", + }, + input: {}, + label: { color: "system.error" }, + }, + false: {}, + }, + }, + defaultVariants: { + error: false, + disabled: false, + size: "md", + }, +}); + +const affix = cva({ + base: { + ...hstack.raw({ alignItems: "center", gap: "1" }), + userSelect: "none", + textStyle: "label.md.default", + }, + variants: { + position: { + prefix: { pl: "3", mr: "-3" }, + suffix: { pr: "3", ml: "-3" }, + }, + }, +}); + +interface InputProps + extends Omit< + React.InputHTMLAttributes, + "prefix" | "suffix" | "size" + >, + React.RefAttributes { + // Rendering + label?: string; + placeholder?: string; + prefix?: string; + suffix?: string; + suggestion?: string; + helper?: string; + // Control + id?: string; + value: string | undefined; + onChange?: React.ChangeEventHandler; + disabled?: boolean; + error?: string | null; + type?: "text" | "number"; + size?: "md" | "sm"; +} +export default function Input({ + // Rendering + label, + placeholder, + prefix, + suffix, + suggestion, + helper, + // Control + id, + ref, + value, + onChange, + disabled = false, + error, + type, + size = "md", + ...props +}: InputProps) { + const randomId = useId(); + const inputId = id ?? randomId; + const errorMessageId = `${inputId}-error`; + + const classes = input({ disabled, error: !!error, size }); + + return ( +
+ {label && ( + + )} + +
+ {prefix && ( +
+ {prefix} + {/* In the designs this vertical bar is defined as a Text, but it's + not. Use a Label instead */} + | +
+ )} + + {suggestion && {suggestion}} + + + + {suffix && ( +
+ {/* In the designs this vertical bar is defined as a Text, but it's + not. Use a Label instead */} + | + {suffix} +
+ )} +
+ + {helper && !error &&

{helper}

} + {error && ( + + )} +
+ ); +} diff --git a/frontend/src/renderer/src/components/ui/link.tsx b/frontend/src/renderer/src/components/ui/link.tsx new file mode 100644 index 00000000..53042173 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/link.tsx @@ -0,0 +1,33 @@ +import { type RecipeVariantProps, css, cx } from "@/styled/css"; +import { Link as TanstackLink, type LinkProps as TanstackLinkProps } from "@tanstack/react-router"; +import { CircleNotch } from "phosphor-react"; +import { button } from "./button"; + +type LinkProps = RecipeVariantProps & + TanstackLinkProps & { + isLoading?: boolean; + className?: string; + }; + +export default function Link({ + size, + variant, + checked, + isLoading, + children, + className, + ...props +}: LinkProps) { + return ( + + {isLoading ? ( + + ) : ( + children + )} + + ); +} diff --git a/frontend/src/renderer/src/components/ui/popover.tsx b/frontend/src/renderer/src/components/ui/popover.tsx new file mode 100644 index 00000000..8be12b91 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/popover.tsx @@ -0,0 +1,53 @@ +import { css, cx } from "@/styled/css"; +import * as PopoverPrimitive from "@radix-ui/react-popover"; +import type { ComponentPropsWithoutRef } from "react"; + +const Popover = PopoverPrimitive.Root; +const PopoverTrigger = PopoverPrimitive.Trigger; +const PopoverAnchor = PopoverPrimitive.Anchor; +const PopoverClose = PopoverPrimitive.Close; + +const contentStyles = css({ + zIndex: 50, + bg: "bg.primary", + rounded: "lg", + boxShadow: "[0px 0px 15px 0px #00000026]", + + "&[data-state='open']": { + animation: "fadeIn", + }, + "&[data-state='closed']": { + animation: "fadeOut", + }, +}); + +const arrowStyles = css({ + fill: "bg.secondary", +}); + +function PopoverContent({ + className, + sideOffset = 8, + showArrow = false, + container, + children, + ...props +}: ComponentPropsWithoutRef & { + showArrow?: boolean; + container?: HTMLElement; +}) { + return ( + + + {children} + {showArrow && } + + + ); +} + +export { Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger }; diff --git a/frontend/src/renderer/src/components/ui/select.tsx b/frontend/src/renderer/src/components/ui/select.tsx new file mode 100644 index 00000000..b04110a1 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/select.tsx @@ -0,0 +1,306 @@ +import * as RadixSelect from "@radix-ui/react-select"; +import { CaretDown, CaretUp, Check } from "phosphor-react"; +import { type Ref, useId, useImperativeHandle } from "react"; + +import Suggestion from "@/components/ui/suggestion"; +import { sva } from "@/styled/css"; +import { styled } from "@/styled/jsx"; +import { stack } from "@/styled/patterns"; + +const Affix = styled("span", { + base: { + textStyle: "label.md.default", + color: "text.lighter", + flexShrink: "0", + }, +}); + +export type SelectOption = { id: string; text: string; shortText?: string }; +export type SelectSuggestion = { id: string; text?: string }; + +interface SelectProps { + options: SelectOption[]; + label?: string; + value?: string; + onChange?: (value: SelectOption) => void; + onOpenChange?: (open: boolean) => void; + prefix?: string; + suffix?: string; + suggestion?: SelectSuggestion; + priorityOrder?: string[]; + placeholder?: string; + disabled?: boolean; + size?: "md" | "sm"; + ref?: Ref<{ value: string | undefined }>; +} + +function orderByPriority(options: SelectOption[], priority: string[] = []) { + const filtered = options.filter(({ id }) => !priority.includes(id)); + const preferred = priority + .map((p) => options.find(({ id }) => p === id)) + .filter((o): o is SelectOption => !!o); + return [...preferred, ...filtered]; +} + +function secureSuggestion( + suggestion: SelectSuggestion | undefined, + options: SelectOption[], +): SelectOption | undefined { + if (!suggestion) return undefined; + if (suggestion.text) return { id: suggestion.id, text: suggestion.text }; + return options.find(({ id }) => id === suggestion.id); +} + +const select = sva({ + slots: [ + "container", + "trigger", + "value", + "caret", + "content", + "viewport", + "item", + "itemIndicator", + "scrollButton", + ], + base: { + container: { ...stack.raw({ gap: "1" }), width: "full" }, + trigger: { + display: "flex", + alignItems: "center", + gap: "2", + width: "full", + bg: "white", + border: "primary", + rounded: "sm", + cursor: "pointer", + textAlign: "left", + appearance: "none", + textStyle: "label.md.default", + color: "text.default", + + "&[data-state='open']": { + boxShadow: "[0px 2px 2px rgba(0, 0, 0, 0.16)]", + borderColor: "[#110041]", + }, + "&:focus-visible": { + outlineColor: "brand.primary", + outlineWidth: "0.5", + outlineStyle: "solid", + outlineOffset: "0.5", + }, + "&[data-disabled]": { + bg: "bg.primary", + cursor: "not-allowed", + color: "text.lighter", + }, + "&[data-placeholder]": { + color: "text.lighter", + }, + }, + value: { + flex: "[1]", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }, + caret: { + flexShrink: "0", + color: "text.default", + transition: "[transform 0.15s ease]", + + "[data-state='open'] > &": { + transform: "[rotate(180deg)]", + }, + }, + content: { + bg: "white", + rounded: "sm", + boxShadow: "[0px 8px 16px rgba(0, 0, 0, 0.08)]", + border: "secondary", + overflow: "hidden", + zIndex: "10", + minWidth: "[var(--radix-select-trigger-width)]", + }, + viewport: { + maxHeight: "[400px]", + overflowY: "auto", + }, + scrollButton: { + display: "flex", + alignItems: "center", + justifyContent: "center", + py: "1", + color: "text.lighter", + cursor: "default", + bg: "white", + }, + item: { + display: "flex", + alignItems: "center", + gap: "2", + textStyle: "label.md.default", + color: "text.default", + cursor: "pointer", + outline: "none", + userSelect: "none", + + "&[data-highlighted]": { + bg: "bg.primary-alternative", + }, + "&[data-state='checked']": { + bg: "bg.primary-alternative", + }, + "&[data-disabled]": { + cursor: "not-allowed", + color: "text.lighter", + }, + }, + itemIndicator: { + color: "brand.primary", + display: "flex", + alignItems: "center", + flexShrink: "0", + }, + }, + variants: { + size: { + md: { + trigger: { px: "3", py: "3" }, + item: { px: "3", py: "3" }, + }, + sm: { + trigger: { px: "3", py: "1" }, + item: { px: "3", py: "3" }, + }, + }, + }, + defaultVariants: { + size: "md", + }, +}); + +export default function Select({ + options, + label, + value, + onChange, + onOpenChange, + prefix, + suffix, + suggestion, + priorityOrder = [], + placeholder = "", + disabled = false, + size = "md", + ref, +}: SelectProps) { + const triggerId = useId(); + const classes = select({ size }); + + const orderedOptions = orderByPriority(options, priorityOrder); + const securedSuggestion = secureSuggestion(suggestion, options); + + useImperativeHandle(ref, () => ({ value }), [value]); + + const handleChange = (id: string) => { + const option = options.find((o) => o.id === id); + if (option) onChange?.(option); + }; + + const handleSuggestionClick = (e: React.MouseEvent) => { + e.stopPropagation(); + if (securedSuggestion) handleChange(securedSuggestion.id); + }; + + const handleSuggestionKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" || e.key === " ") { + e.stopPropagation(); + if (securedSuggestion) handleChange(securedSuggestion.id); + } + }; + + return ( +
+ {label && ( + + {label} + + )} + + + + {/* biome-ignore lint/a11y/useSemanticElements lint/a11y/useAriaPropsForRole: Radix merges role, aria-expanded, aria-controls, and tabIndex onto this div at runtime via asChild */} +
+ {prefix && } + + + + + + {!value && securedSuggestion ? ( + + ) : ( + + {value + ? (options.find((o) => o.id === value)?.shortText ?? + options.find((o) => o.id === value)?.text ?? + placeholder) + : undefined} + + )} + + + {suffix && } +
+
+ + + + + + + + {orderedOptions.map(({ id, text }) => ( + + + + + {text} + + ))} + + + + + + +
+
+ ); +} diff --git a/frontend/src/renderer/src/components/ui/suggestion.tsx b/frontend/src/renderer/src/components/ui/suggestion.tsx new file mode 100644 index 00000000..d41aa5a4 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/suggestion.tsx @@ -0,0 +1,25 @@ +import { styled } from "@/styled/jsx"; + +const Suggestion = styled("mark", { + base: { + bg: "bg.primary-alternative", + textStyle: "label.md.default", + display: "inline-flex", + }, + variants: { + clickable: { + true: { cursor: "pointer" }, + false: { cursor: "unset" }, + }, + rounded: { + true: { rounded: "md" }, + false: { rounded: "[0px]" }, + }, + }, + defaultVariants: { + clickable: false, + rounded: false, + }, +}); + +export default Suggestion; diff --git a/frontend/src/renderer/src/components/ui/switch.tsx b/frontend/src/renderer/src/components/ui/switch.tsx new file mode 100644 index 00000000..dfb2442b --- /dev/null +++ b/frontend/src/renderer/src/components/ui/switch.tsx @@ -0,0 +1,57 @@ +import * as RadixSwitch from "@radix-ui/react-switch"; +import { css } from "@/styled/css"; + +const rootStyle = css({ + width: "[44px]", + height: "[24px]", + borderRadius: "full", + border: "none", + cursor: "pointer", + position: "relative", + flexShrink: "0", + bg: "bg.primary-highlight", + + transitionProperty: "[background-color]", + transitionDuration: "slow", + transitionTimingFunction: "default", + + '&[data-state="checked"]': { + bg: "brand.primary", + }, + + "&:disabled": { + cursor: "not-allowed", + opacity: "0.4", + }, +}); + +const thumbStyle = css({ + display: "block", + width: "[20px]", + height: "[20px]", + borderRadius: "full", + bg: "white", + position: "absolute", + top: "[2px]", + left: "[2px]", + + transitionProperty: "[transform]", + transitionDuration: "slow", + transitionTimingFunction: "default", + + '&[data-state="checked"]': { + transform: "[translateX(20px)]", + }, +}); + +type SwitchProps = RadixSwitch.SwitchProps; + +function Switch(props: SwitchProps) { + return ( + + + + ); +} + +export default Switch; diff --git a/frontend/src/renderer/src/components/ui/toast.tsx b/frontend/src/renderer/src/components/ui/toast.tsx new file mode 100644 index 00000000..48f935a3 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/toast.tsx @@ -0,0 +1,35 @@ +import type { Icon } from "phosphor-react"; +import { type Toast as HotToast, toast } from "react-hot-toast"; +import Callout, { type CalloutVariant } from "./callout"; + +export type ToastVariant = CalloutVariant; + +interface ToastProps { + t: HotToast; + message: string; + variant?: ToastVariant; + icon?: Icon; +} + +const ASSERTIVE_VARIANTS: ToastVariant[] = ["error", "warning"]; + +function Toast({ t, message, variant = "info", icon }: ToastProps) { + const isAssertive = ASSERTIVE_VARIANTS.includes(variant); + + return ( +
+ toast.remove(t.id)} + role={isAssertive ? "alert" : "status"} + aria-live={isAssertive ? "assertive" : "polite"} + aria-atomic="true" + noBorder + /> +
+ ); +} + +export default Toast; diff --git a/frontend/src/renderer/src/components/ui/tooltip.tsx b/frontend/src/renderer/src/components/ui/tooltip.tsx new file mode 100644 index 00000000..537cf185 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/tooltip.tsx @@ -0,0 +1,50 @@ +import { css, cx } from "@/styled/css"; +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; +import type { ComponentPropsWithoutRef } from "react"; + +const TooltipProvider = TooltipPrimitive.Provider; +const Tooltip = TooltipPrimitive.Root; +const TooltipTrigger = TooltipPrimitive.Trigger; + +const contentStyles = css({ + zIndex: 50, + bg: "bg.primary", + rounded: "md", + boxShadow: "[0px 0px 15px 0px #00000026]", + + "&[data-state='delayed-open']": { + animation: "fadeIn", + }, + "&[data-state='closed']": { + animation: "fadeOut", + }, +}); + +const arrowStyles = css({ + fill: "bg.primary", +}); + +function TooltipContent({ + className, + sideOffset = 6, + showArrow = true, + children, + ...props +}: ComponentPropsWithoutRef & { + showArrow?: boolean; +}) { + return ( + + + {children} + {showArrow && } + + + ); +} + +export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }; diff --git a/frontend/src/renderer/src/components/uncontrolled-input/UncontrolledInput.styles.ts b/frontend/src/renderer/src/components/uncontrolled-input/UncontrolledInput.styles.ts new file mode 100644 index 00000000..991ff6f9 --- /dev/null +++ b/frontend/src/renderer/src/components/uncontrolled-input/UncontrolledInput.styles.ts @@ -0,0 +1,43 @@ +import { styled } from "@/styles"; + +export const Container = styled("label", { + display: "flex", + flexDirection: "column", + gap: "$xs", + + fontWeight: 400, + fontSize: "$labelSm", + lineHeight: "$labelSm", + + color: "$textDefault", +}); + +export const InputContainer = styled("div", { + display: "flex", + flexDirection: "row", + alignItems: "center", + gap: "$s", + flex: 1, + + p: 12, + + bg: "$bgSecondary", + b: "1px solid $borderPrimary", + borderRadius: "$xs", + + "&:focus": { + outline: "none", + b: "1px solid $borderPrimaryAlt", + boxShadow: "0px 2px 2px rgba(0, 0, 0, 0.16)", + }, +}); +export const Input = styled("input", { + fontWeight: 400, + fontSize: "$labelMd", + lineHeight: "$labelMd", + + b: "none", + outline: "none", + + flex: 1, +}); diff --git a/frontend/src/renderer/src/components/uncontrolled-input/index.tsx b/frontend/src/renderer/src/components/uncontrolled-input/index.tsx new file mode 100644 index 00000000..0e777e7b --- /dev/null +++ b/frontend/src/renderer/src/components/uncontrolled-input/index.tsx @@ -0,0 +1,141 @@ +import { + type ChangeEventHandler, + type KeyboardEvent, + type ReactNode, + useImperativeHandle, + useState, +} from "react"; + +import Label from "@/components/label"; +import Text from "@/components/text"; +import Suggestion from "@/components/ui/suggestion"; +import type { CSS } from "@/styles"; +import type { NativeComponent } from "@/types/component"; +import { forwardRef } from "react"; +import { + Container, + InputContainer, + Input as StyledInput, +} from "./UncontrolledInput.styles"; + +export type InputRefValue = { value: string }; +interface Props + extends NativeComponent<"input", "prefix" | "type" | "value" | "onChange"> { + label?: string; + suggestion?: string; + helper?: string; + sufix?: ReactNode; + prefix?: ReactNode; + defaultValue?: string; + onChange?: (value: string) => void; + type?: "text" | "number"; + css?: CSS; + specialCharacters?: string; +} +export default forwardRef<{ value: string }, Props>(function UncontrolledInput( + { + label, + helper, + suggestion, + prefix, + sufix, + defaultValue, + onChange, + type = "text", + specialCharacters = "", + ...props + }, + ref, +) { + const [value, setValue] = useState(defaultValue ?? ""); + + // Only exposes `selected` object to the parent component + useImperativeHandle( + ref, + () => { + return { + value, + }; + }, + [value], + ); + + const isValueEmpty = !value || value === ""; + + const updateValue = (newValue: string) => { + if (type === "number") { + const regex = new RegExp(`^[\\d${specialCharacters}.]+$`); + if (regex.test(newValue) || !newValue) { + setValue(newValue); + onChange?.(newValue); + return; + } + return; + } + + setValue(newValue); + + onChange?.(newValue); + }; + + const handleClickSuggestion = () => { + updateValue(suggestion as string); // We are sure is a string because the button is enabled only in case suggestion = string + }; + const handleKeySuggestion = (e: KeyboardEvent) => { + e.preventDefault(); + + if (e.code === "Space" || e.code === "Enter") + updateValue(suggestion as string); // We are sure is a string because the button is enabled only in case suggestion = string + }; + + const handleChange: ChangeEventHandler = (e) => { + updateValue(e.target.value); + }; + + return ( + + {/* LABEL */} + {label} + + {/* INPUT CONTAINER */} + + {/* PREFIX */} + {prefix && ( + <> + + | + + )} + + {/* INPUT */} + + + {/* SUGGESTION */} + {suggestion && isValueEmpty && ( + <> + | + + {suggestion} + + + )} + + {/* SUFIX */} + {sufix} + + + {/* HELPER */} + {helper && } + + ); +}); diff --git a/frontend/src/renderer/src/components/validate-dataset/form-group/FormGroup.styles.ts b/frontend/src/renderer/src/components/validate-dataset/form-group/FormGroup.styles.ts new file mode 100644 index 00000000..60f516d8 --- /dev/null +++ b/frontend/src/renderer/src/components/validate-dataset/form-group/FormGroup.styles.ts @@ -0,0 +1,9 @@ +import { styled } from "@/styles"; + +const Container = styled("div", { + display: "flex", + flexDirection: "column", + gap: 64, +}); + +export default Container; diff --git a/frontend/src/renderer/src/components/validate-dataset/form-group/FormGroup.types.ts b/frontend/src/renderer/src/components/validate-dataset/form-group/FormGroup.types.ts new file mode 100644 index 00000000..47fea702 --- /dev/null +++ b/frontend/src/renderer/src/components/validate-dataset/form-group/FormGroup.types.ts @@ -0,0 +1,17 @@ +import type { FormEventHandler } from "react"; + +import type { FormValue, RegisterFunction } from "@/hooks/useForm"; +import type { LabelDecisiones } from "@/types/aymurai"; +import type Suggester from "@/utils/predictions/suggestions"; + +export interface FormProps { + decision?: number; + register: RegisterFunction; + onSubmit: FormEventHandler; + onCheck: (checked: boolean) => void; + suggester: Suggester; +} + +export interface FormDecisionProps extends FormProps { + getDecisionValue: (n: number) => (field: LabelDecisiones) => FormValue; +} diff --git a/frontend/src/renderer/src/components/validate-dataset/form-group/forms/DatosAcusado.tsx b/frontend/src/renderer/src/components/validate-dataset/form-group/forms/DatosAcusado.tsx new file mode 100644 index 00000000..e8c8a696 --- /dev/null +++ b/frontend/src/renderer/src/components/validate-dataset/form-group/forms/DatosAcusado.tsx @@ -0,0 +1,52 @@ +import { Input, ValidationForm } from "@/components"; +import Select from "@/components/ui/select"; +import { LabelType } from "@/types/aymurai"; +import type { FormProps } from "../FormGroup.types"; +import json from "./options.json"; + +export default function DatosAcusado({ + onSubmit, + onCheck, + register, + suggester, +}: FormProps) { + return ( + + + + + + + + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/validate-dataset/form-group/forms/InfoGral.tsx b/frontend/src/renderer/src/components/validate-dataset/form-group/forms/InfoGral.tsx new file mode 100644 index 00000000..e2548216 --- /dev/null +++ b/frontend/src/renderer/src/components/validate-dataset/form-group/forms/InfoGral.tsx @@ -0,0 +1,55 @@ +import { Input, ValidationForm } from "@/components"; +import { LabelType } from "@/types/aymurai"; +import type { FormProps } from "../FormGroup.types"; + +export default function InfoGral({ + onSubmit, + onCheck, + register, + suggester, +}: FormProps) { + return ( + + + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/validate-dataset/form-group/forms/InfoHecho.tsx b/frontend/src/renderer/src/components/validate-dataset/form-group/forms/InfoHecho.tsx new file mode 100644 index 00000000..fcb71365 --- /dev/null +++ b/frontend/src/renderer/src/components/validate-dataset/form-group/forms/InfoHecho.tsx @@ -0,0 +1,246 @@ +import { Plus } from "phosphor-react"; +import { useState } from "react"; + +import { + Button, + Checkbox, + CheckboxGroup, + Input, + Radio, + RadioGroup, + Stack, + ValidationForm, +} from "@/components"; +import Select from "@/components/ui/select"; +import { css } from "@/styled/css"; +import { LabelDecisiones } from "@/types/aymurai"; +import nArray from "@/utils/nArray"; +import type { FormDecisionProps } from "../FormGroup.types"; +import json from "./options.json"; + +export default function DatosDenunciante({ + decision, + onSubmit, + onCheck, + register, + suggester, + getDecisionValue, +}: FormDecisionProps) { + const [frasesAgresion, setFrasesAgresion] = useState(1); + + const newFraseAgresion = () => setFrasesAgresion(frasesAgresion + 1); + + const frasesArray = nArray(frasesAgresion, undefined).map((_, i) => i); + const fraseName = (i: number) => `${LabelDecisiones.FRASES_AGRESION}${i}`; + const defaultValue = getDecisionValue(decision ?? 0); + const prop = (label: LabelDecisiones) => register(label, decision); + + return ( + + + + + + {frasesArray.map((i) => ( + 1 ? i + 1 : undefined} + ref={prop(fraseName(i) as LabelDecisiones)} // Treat this dynamic Label the same way as any other + label="Frases de la agresión" + {...suggester.text(fraseName(i) as LabelDecisiones)} + /> + ))} + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/validate-dataset/form-group/forms/index.ts b/frontend/src/renderer/src/components/validate-dataset/form-group/forms/index.ts new file mode 100644 index 00000000..0acd781a --- /dev/null +++ b/frontend/src/renderer/src/components/validate-dataset/form-group/forms/index.ts @@ -0,0 +1,7 @@ +import DatosAcusado from "./DatosAcusado"; +import DatosDenunciante from "./DatosDenunciante"; +import Decision from "./Decision"; +import InfoGral from "./InfoGral"; +import InfoHecho from "./InfoHecho"; + +export { InfoGral, DatosDenunciante, DatosAcusado, InfoHecho, Decision }; diff --git a/frontend/src/renderer/src/components/validate-dataset/form-group/forms/options.json b/frontend/src/renderer/src/components/validate-dataset/form-group/forms/options.json new file mode 100644 index 00000000..a35142a8 --- /dev/null +++ b/frontend/src/renderer/src/components/validate-dataset/form-group/forms/options.json @@ -0,0 +1,1937 @@ +{ + "GENERO": [ + { "id": "varon_cis", "text": "Varón cis" }, + { "id": "mujer_cis", "text": "Mujer cis" }, + { "id": "varon_trans", "text": "Varón trans" }, + { "id": "mujer_trans", "text": "Mujer trans" }, + { "id": "travesti", "text": "Travesti" }, + { "id": "no_binaria", "text": "No binaria" }, + { "id": "sd", "text": "S/D" }, + { "id": "no_corresponde", "text": "No corresponde" } + ], + "ZONA_DEL_HECHO": [ + { "id": "zona_sur", "text": "Zona Sur" }, + { "id": "zona_este", "text": "Zona Este" }, + { "id": "zona_oeste", "text": "Zona Oeste" }, + { "id": "zona_norte", "text": "Zona Norte" }, + { "id": "provincia_de_buenos_aires", "text": "Provincia de Buenos Aires" }, + { "id": "no_corresponde", "text": "No corresponde" }, + { "id": "mas_de_una_zona", "text": "Más de una zona" }, + { "id": "s/d", "text": "S/D" } + ], + "LUGAR_DEL_HECHO": [ + { "id": "en_auto_particular", "text": "En auto particular" }, + { "id": "en_comisaria", "text": "En comisaría" }, + { + "id": "en_complejo_penitenciario", + "text": "En complejo penitenciario" + }, + { "id": "en_domicilio_laboral", "text": "En domicilio laboral" }, + { + "id": "en_domicilio_laboral_y_mediante_medios_tecnologicos", + "text": "En domicilio laboral y mediante medios tecnológicos" + }, + { "id": "en_domicilio_particular", "text": "En domicilio particular" }, + { + "id": "en_domicilio_particular_y_en_puerta_de_domicilio_particular", + "text": "En domicilio particular y en puerta de domicilio particular" + }, + { + "id": "en_domicilio_particular_y_mediante_medios_tecnologicos", + "text": "En domicilio particular y mediante medios tecnológicos" + }, + { "id": "en_estadio_de_futbol", "text": "En estadio de fútbol" }, + { "id": "en_geriatrico", "text": "En geriátrico" }, + { "id": "en_hospital", "text": "En hospital" }, + { "id": "en_institucion_privada", "text": "En institución privada" }, + { "id": "en_lugar_privado", "text": "En lugar privado" }, + { "id": "en_lugar_publico", "text": "En lugar público" }, + { + "id": "en_puerta_de_domicilio_particular_y_mediante_medios_tecnológicos", + "text": "En puerta de domicilio particular y mediante medios tecnológicos" + }, + { "id": "en_transporte_publico", "text": "En transporte público" }, + { + "id": "en_una_institucion_educativa", + "text": "En una institución educativa" + }, + { + "id": "en_via_publica_y_mediante_medios_tecnologicos", + "text": "En vía pública y mediante medios tecnológicos" + }, + { + "id": "mediante_medios_tecnologicos", + "text": "Mediante medios tecnológicos" + }, + { "id": "pasillo_de_domicilio", "text": "Pasillo de domicilio" }, + { + "id": "puerta_de_domicilio_particular_y_en_domicilio_laboral", + "text": "Puerta de domicilio particular y en domicilio laboral" + }, + { + "id": "puerta_de_domicilio_particular", + "text": "Puerta de domicilio particular" + }, + { + "id": "puerta_de_domicilio_particular_y_en_puerta_de_domicilio_laboral", + "text": "Puerta de domicilio particular y en puerta de domicilio laboral" + }, + { "id": "via_publica", "text": "Vía pública" }, + { + "id": "via_publica_y_en_domicilio_particular", + "text": "Via pública y en domicilio particular" + }, + { + "id": "via_publica_y_en_domicilio_particular_y_mediante_medios_tecnologicos", + "text": "Vía pública y en domicilio particular y mediante medios tecnológicos" + } + ], + "FRECUENCIA_EPISODIOS": [ + { "id": "primera_vez", "text": "Primera vez" }, + { "id": "esporadico", "text": "Esporádico" }, + { "id": "diario", "text": "Diario" }, + { "id": "habitual", "text": "Habitual" }, + { "id": "eventual", "text": "Eventual" }, + { "id": "sd", "text": "S/D" }, + { "id": "no_corresponde", "text": "No corresponde" } + ], + "NIVEL_INSTRUCCION": [ + { "id": "sin_instruccion", "text": "Sin instruccion" }, + { "id": "sin_escolarizar", "text": "Sin escolarizar" }, + { "id": "primario_incompleto", "text": "Primario incompleto" }, + { "id": "primario_completo", "text": "Primario completo" }, + { "id": "primario_en_curso", "text": "Primario en curso" }, + { "id": "secundario_incompleto", "text": "Secundario incompleto" }, + { "id": "secundario_completo", "text": "Secundario completo" }, + { "id": "secundario_en_curso", "text": "Secundario en curso" }, + { "id": "terciario_incompleto", "text": "Terciario incompleto" }, + { "id": "terciario_completo", "text": "Terciario completo" }, + { "id": "terciario_en_curso", "text": "Terciario en curso" }, + { + "id": "universitario_incompleto", + "text": "Universitario incompleto" + }, + { "id": "universitario_completo", "text": "Universitario completo" }, + { "id": "universitario_en_curso", "text": "Universitario en curso" }, + { "id": "sd", "text": "S/D" }, + { "id": "no_corresponde", "text": "No corresponde" } + ], + "MODALIDAD_DE_LA_VIOLENCIA": [ + { "id": "domestica", "text": "Doméstica" }, + { "id": "en_espacio_publico", "text": "En espacio público" }, + { "id": "institucional", "text": "Institucional" }, + { "id": "laboral", "text": "Laboral" }, + { "id": "libertad_reproductiva", "text": "Libertad reproductiva" }, + { "id": "mediatica", "text": "Mediática" }, + { "id": "no_corresponde", "text": "No corresponde" }, + { "id": "obstetrica", "text": "Obstétrica" }, + { "id": "publica_politica", "text": "Pública política" } + ], + "TIPO_DE_RESOLUCION": [ + { "id": "definitiva", "text": "Definitiva" }, + { "id": "interlocutoria", "text": "Interlocutoria" } + ], + "DECISION": [ + { "id": "hace_lugar", "text": "Hace lugar" }, + { "id": "no_hace_lugar", "text": "No hace lugar" } + ], + "CONDUCTA": [ + { + "id": "abandonar_animal_domestico", + "text": "Abandonar animal doméstico" + }, + { "id": "abandono_de_personas", "text": "Abandono de personas" }, + { "id": "abuso_de_armas", "text": "Abuso de armas" }, + { + "id": "abuso_de_autoridad_e_incumplimiento_deberes_funcionario_publico", + "text": "Abuso de autoridad e incumplimiento deberes funcionario público" + }, + { "id": "abuso_sexual", "text": "Abuso sexual" }, + { + "id": "acceder_lugares_distintos_segun_entrada", + "text": "Acceder lugares distintos según entrada" + }, + { + "id": "acceder_a_lugares_distintos_segun_entrada", + "text": "Acceder a lugares distintos según entrada" + }, + { + "id": "acceso_a_sistema_restringido", + "text": "Acceso a sistema restringido" + }, + { "id": "acoso_sexual_callejero", "text": "Acoso sexual callejero" }, + { + "id": "actividades_lucrativas_sin_autorizacion", + "text": "Actividades lucrativas sin autorización" + }, + { + "id": "actividades_lucrativas_sin_habilitacion", + "text": "Actividades lucrativas sin habilitación" + }, + { + "id": "actos_contenido_sexual_con_menores", + "text": "Actos contenido sexual con menores" + }, + { + "id": "administracion_fraudulenta", + "text": "Administración fraudulenta" + }, + { + "id": "afectar_desarrollo_espectaculo", + "text": "Afectar desarrollo espectáculo" + }, + { + "id": "afectar_el_desarrollo_del_espectaculo", + "text": "Afectar el desarrollo del espectáculo" + }, + { + "id": "afectar_funcionamiento_servicios_publicos", + "text": "Afectar funcionamiento servicios públicos" + }, + { + "id": "afectar_servicios_de_emergencia_o_seguridad", + "text": "Afectar servicios de emergencia o seguridad" + }, + { + "id": "afectar_servicios_emergencia", + "text": "Afectar servicios emergencia" + }, + { "id": "afectar_señalizacion", "text": "Afectar señalización" }, + { "id": "allanamiento_autonomo", "text": "Allanamiento autónomo" }, + { "id": "alterar_programa", "text": "Alterar programa" }, + { "id": "alterar_sepulturas", "text": "Alterar sepulturas" }, + { "id": "amenazas", "text": "Amenazas" }, + { "id": "amparo", "text": "Amparo" }, + { "id": "apariencia_falsa", "text": "Apariencia falsa" }, + { + "id": "apariencia_falsa_para_entrar_a_domicilio_o_lugar_privado", + "text": "Apariencia falsa para entrar a domicilio o lugar privado" + }, + { + "id": "apropiacion_indebida_de_tributos", + "text": "Apropiacion indebida de tributos" + }, + { + "id": "arrojar_cosas_que_puedan_causar_lesiones", + "text": "Arrojar cosas que puedan causar lesiones" + }, + { "id": "arrojar_cosas_sustancias", "text": "Arrojar cosas sustancias" }, + { + "id": "arrojar_sustancias_insalubres_en_lugares_publicos", + "text": "Arrojar sustancias insalubres en lugares públicos" + }, + { "id": "asociacion_ilicita", "text": "Asociacion ilícita" }, + { + "id": "asuncion_falsa_de_contravencion", + "text": "Asunción falsa de contravención" + }, + { + "id": "atentado_contra_la_autoridad", + "text": "Atentado contra la autoridad" + }, + { "id": "ausencia_de_habilitacion", "text": "Ausencia de habilitación" }, + { "id": "banderas", "text": "Banderas" }, + { "id": "carteles_afiches_volantes", "text": "Carteles afiches volantes" }, + { "id": "cierre_defectuoso", "text": "Cierre defectuoso" }, + { + "id": "circular_por_carriles_exclusivos", + "text": "Circular por carriles exclusivos" + }, + { + "id": "circular_por_lugar_prohibido", + "text": "Circular por lugar prohibido" + }, + { "id": "cohecho_activo", "text": "Cohecho activo" }, + { "id": "cohecho_pasivo", "text": "Cohecho pasivo" }, + { "id": "coimas", "text": "Coimas" }, + { "id": "competencia_desleal", "text": "Competencia desleal" }, + { + "id": "conducir_con_mayor_cantidad_de_alcohol_en_sangre_del_permitido", + "text": "Conducir con mayor cantidad de alcohol en sangre del permitido" + }, + { + "id": "conducir_con_mayor_grado_de_alcohol_o_bajo_efectos_de_estupefacientes", + "text": "Conducir con mayor grado de alcohol o bajo efectos de estupefacientes" + }, + { + "id": "conducir_sin_licencia_que_lo_habilite_por_categoria_de_vehiculo", + "text": "Conducir sin licencia que lo habilite por categoría de vehiculo" + }, + { + "id": "conducir_usando_aparato_electronico", + "text": "Conducir usando aparato electrónico" + }, + { "id": "connivencia_policial", "text": "Connivencia policial" }, + { + "id": "contactar_menor_por_medio_de_tecnologias_para_cometer_delitos_contra_su_integridad_sexual", + "text": "Contactar menor por medio de tecnologías para cometer delitos contra su integridad sexual" + }, + { "id": "cruce_de_semaforo_en_rojo", "text": "Cruce de semaforo en rojo" }, + { "id": "cuida_coche", "text": "Cuida coche" }, + { + "id": "cuidar_coches_sin_autorizacion", + "text": "Cuidar coches sin autorización" + }, + { "id": "daños", "text": "Daños" }, + { "id": "daños_informaticos", "text": "Daños informáticos" }, + { "id": "defraudacion", "text": "Defraudación" }, + { + "id": "defraudacion_contra_la_administracion_publica", + "text": "Defraudación contra la administración pública" + }, + { + "id": "delito_contra_seguridad_transito", + "text": "Delito contra seguridad tránsito" + }, + { "id": "denegacion_de_justicia", "text": "Denegación de justicia" }, + { "id": "derecho_admision", "text": "Derecho admisión" }, + { "id": "desarmado_automotor", "text": "Desarmado automotor" }, + { + "id": "desinfeccion_y_desratizacion", + "text": "Desinfección y desratización" + }, + { + "id": "desobediencia_a_cargas_procesales", + "text": "Desobediencia a cargas procesales" + }, + { + "id": "desobediencia_a_la_autoridad", + "text": "Desobediencia a la autoridad" + }, + { + "id": "difusion_no_autorizada_de_imagenes", + "text": "Difusión no autorizada de imágenes" + }, + { + "id": "difusion_no_autorizada_de_imagenes_intimas", + "text": "Difusion no autorizada de imágenes íntimas" + }, + { "id": "discriminar", "text": "Discriminar" }, + { "id": "documentacion_sanitaria", "text": "Documentación sanitaria" }, + { "id": "ejecucion_de_multa", "text": "Ejecución de multa" }, + { + "id": "ejercer_ilegitimamente_actividad", + "text": "Ejercer ilegítimamente actividad" + }, + { + "id": "ejercicio_ilegal_de_la_medicina", + "text": "Ejercicio ilegal de la medicina" + }, + { + "id": "elementos_de_prevencion_contra_incendio", + "text": "Elementos de prevención contra incendio" + }, + { "id": "encubrimiento", "text": "Encubrimiento" }, + { + "id": "encubrimiento_actividades_baile", + "text": "Encubrimiento actividades baile" + }, + { "id": "ensuciar_bienes", "text": "Ensuciar bienes" }, + { + "id": "entregar_indebidamente_armas_explosivos", + "text": "Entregar indebidamente armas explosivos" + }, + { "id": "espantar_animales", "text": "Espantar animales" }, + { "id": "espejos_retrovisores", "text": "Espejos retrovisores" }, + { "id": "estacionamiento_prohibido", "text": "Estacionamiento prohibido" }, + { + "id": "estacionamiento_sobre_senda_peatonal", + "text": "Estacionamiento sobre senda peatonal" + }, + { "id": "estafa", "text": "Estafa" }, + { "id": "evasion_tributaria", "text": "Evasión tributaria" }, + { "id": "estupefacientes", "text": "Estupefacientes" }, + { "id": "exceso_capacidad_ingreso", "text": "Exceso capacidad ingreso" }, + { "id": "exceso_de_velocidad", "text": "Exceso de velocidad" }, + { + "id": "exhibicion_de_documentacion_obligatoria", + "text": "Exhibición de documentacion obligatoria" + }, + { "id": "exhibiciones_obscenas", "text": "Exhibiciones obscenas" }, + { + "id": "exhortos_de_otras_jurisdicciones", + "text": "Exhortos de otras jurisdicciones" + }, + { + "id": "explotacion_trabajo_infantil", + "text": "Explotación trabajo infantil" + }, + { "id": "extorsion", "text": "Extorsión" }, + { + "id": "fabricar_transportar_artefactos_pirotecnicos", + "text": "Fabricar transportar artefactos pirotécnicos" + }, + { "id": "falsa_denuncia", "text": "Falsa denuncia" }, + { + "id": "falsedad_documental_de_certificado_de_aptitud_ambiental", + "text": "Falsedad documental de certificado de aptitud ambiental" + }, + { + "id": "falsificacion_certificado_medico", + "text": "Falsificación certificado médico" + }, + { + "id": "falsificacion_de_documento", + "text": "Falsificación de documento" + }, + { + "id": "falsificacion_de_marcas_firmas_señas_oficiales", + "text": "Falsificación de marcas firmas señas oficiales" + }, + { + "id": "falsificacion_de_numeracion_de_objeto_registrada_de_acuerdo_a_ley", + "text": "Falsificación de numeración de objeto registrada de acuerdo a ley" + }, + { + "id": "falta_de_poliza_de_seguros", + "text": "Falta de póliza de seguros" + }, + { + "id": "favorecimiento_evasion_persona_detenida", + "text": "Favorecimiento evasión persona detenida" + }, + { "id": "frustrar_subasta_publica", "text": "Frustrar subasta pública" }, + { + "id": "guardar_artefactos_pirotecnicos", + "text": "Guardar artefactos pirotécnicos" + }, + { + "id": "guardar_elementos_aptos_violencia", + "text": "Guardar elementos aptos violencia" + }, + { "id": "habeas_corpus", "text": "Habeas corpus" }, + { + "id": "habilitacion_en_infraccion", + "text": "Habilitación en infracción" + }, + { "id": "homicidio", "text": "Homicidio" }, + { "id": "hostigamiento", "text": "Hostigamiento" }, + { "id": "hostigamiento_digital", "text": "Hostigamiento digital" }, + { "id": "hurto", "text": "Hurto" }, + { + "id": "impedimento_de_contacto_de_menor_con_padre_no_conviviente", + "text": "Impedimento de contacto de menor con padre no conviviente" + }, + { + "id": "incendio_explosion_inundacion_con_peligro_para_bienes", + "text": "Incendio explosión inundación con peligro para bienes" + }, + { + "id": "incendios_y_otros_estragos", + "text": "Incendios y otros estragos" + }, + { "id": "incitar_desorden", "text": "Incitar desorden" }, + { "id": "incitar_al_desorden", "text": "Incitar al desorden" }, + { "id": "incumplimiento_de_plazos", "text": "Incumplimiento de plazos" }, + { + "id": "incumplimiento_deberes_familiares", + "text": "Incumplimiento deberes familiares" + }, + { + "id": "incumplimiento_deberes_funcionario_publico", + "text": "Incumplimiento deberes funcionario público" + }, + { + "id": "incumplimiento_medidas_prevencion_sanidad", + "text": "Incumplimiento medidas prevención sanidad" + }, + { + "id": "incumplimiento_perimetro_profundidad", + "text": "Incumplimiento perímetro profundidad" + }, + { "id": "incumplir_clausura", "text": "Incumplir clausura" }, + { + "id": "incumplir_obligaciones_legales", + "text": "Incumplir obligaciones legales" + }, + { "id": "inducir_menor_a_mendigar", "text": "Inducir menor a mendigar" }, + { + "id": "infraccion_reglamentos_seguridad", + "text": "Infracción reglamentos seguridad" + }, + { + "id": "ingresar_artefactos_pirotecnicos", + "text": "Ingresar artefactos pirotécnicos" + }, + { + "id": "ingresar_consumir_bebidas_alcoholicas", + "text": "Ingresar consumir bebidas alcohólicas" + }, + { + "id": "ingresar_contra_derecho_admision", + "text": "Ingresar contra derecho admisión" + }, + { + "id": "ingresar_sin_autorizacion_lugares_reservados", + "text": "Ingresar sin autorización lugares reservados" + }, + { "id": "ingresar_sin_entrada", "text": "Ingresar sin entrada" }, + { + "id": "ingresar_sin_entrada_autorizacion", + "text": "Ingresar sin entrada autorización" + }, + { + "id": "ingreso_a_domicilio_sin_autorizacion", + "text": "Ingreso a domicilio sin autorización" + }, + { "id": "inhumar_exhumar_profanar", "text": "Inhumar exhumar profanar" }, + { "id": "intimidacion", "text": "Intimidación" }, + { "id": "juego_sin_autorizacion", "text": "Juego sin autorización" }, + { + "id": "juegos_de_azar_sin_autorizacion", + "text": "Juegos de azar sin autorización" + }, + { "id": "lesiones", "text": "Lesiones" }, + { "id": "ley_451", "text": "Ley 451" }, + { "id": "licencia_vencida", "text": "Licencia vencida" }, + { "id": "maltrato", "text": "Maltrato" }, + { + "id": "mantener_animal_domestico_espacios_inadecuados", + "text": "Mantener animal doméstico espacios inadecuados" + }, + { + "id": "mantenimiento_de_cercas_y_aceras", + "text": "Mantenimiento de cercas y aceras" + }, + { + "id": "menoscabar_integridad_animal_domestico", + "text": "Menoscabar integridad animal doméstico" + }, + { "id": "mesnna_masnna", "text": "Mesnna masnna" }, + { + "id": "no_realizar_grabado_de_autopartes", + "text": "No realizar grabado de autopartes" + }, + { "id": "no_respetar_carriles", "text": "No respetar carriles" }, + { + "id": "no_respetar_senda_peatonal", + "text": "No respetar senda peatonal" + }, + { + "id": "obligacion_de_exhibir_cartel", + "text": "Obligación de exhibir cartel" + }, + { + "id": "obligacion_de_informar_prohibicion_de_fumar", + "text": "Obligación de informar prohibición de fumar" + }, + { "id": "obligacion_de_conservar", "text": "Obligación de conservar" }, + { + "id": "obstaculizar_ingreso_o_salida", + "text": "Obstaculizar ingreso o salida" + }, + { "id": "obstruccion_de_inspeccion", "text": "Obstrucción de inspección" }, + { "id": "obstruccion_de_via", "text": "Obstrucción de vía" }, + { + "id": "obstruccion_de_via_publica", + "text": "Obstruccion de vía pública" + }, + { + "id": "obstruccion_via_publica", + "text": "Obstruccion vía pública" + }, + { "id": "obstruir_salida", "text": "Obstruir salida" }, + { "id": "ocupar_via_publica", "text": "Ocupar vía pública" }, + { + "id": "oferta_demanda_sexo_espacio_publico", + "text": "Oferta demanda sexo espacio público" + }, + { + "id": "omitir_cuidados_animal_domestico", + "text": "Omitir cuidados animal doméstico" + }, + { + "id": "omitir_recaudos_de_cuidado_animal_domestico", + "text": "Omitir recaudos de cuidado animal doméstico" + }, + { + "id": "omitir_recaudos_de_organizacion", + "text": "Omitir recaudos de organización" + }, + { + "id": "omitir_recaudos_espectaculo_masivo", + "text": "Omitir recaudos espectáculo masivo" + }, + { + "id": "omitir_recaudos_organizacion_seguridad", + "text": "Omitir recaudos organización seguridad" + }, + { "id": "organizar_explotar_juego", "text": "Organizar explotar juego" }, + { + "id": "organizar_sin_autorizacion_juego", + "text": "Organizar sin autorización juego" + }, + { + "id": "participar_competencias_velocidad_via_publica", + "text": "Participar competencias velocidad via pública" + }, + { "id": "pelear_en_lugar_publico", "text": "Pelear en lugar público" }, + { "id": "permiso_y_planos_de_obra", "text": "Permiso y planos de obra" }, + { "id": "persona_no_habilitada", "text": "Persona no habilitada" }, + { + "id": "perturbar_ceremonias_religiosas_funebres", + "text": "Perturbar ceremonias religiosas fúnebres" + }, + { + "id": "perturbar_filas_ingreso_no_respetar_vallado", + "text": "Perturbar filas ingreso no respetar vallado" + }, + { "id": "placas_de_dominio", "text": "Placas de dominio" }, + { "id": "pornografia_infantil", "text": "Pornografía infantil" }, + { "id": "portacion_de_arma", "text": "Portación de arma" }, + { + "id": "portar_armas_no_convencionales", + "text": "Portar armas no convencionales" + }, + { + "id": "presencia_menores_lugar_no_autorizado", + "text": "Presencia menores lugar no autorizado" + }, + { "id": "presunta_comision_delito", "text": "Presunta comisión delito" }, + { "id": "presunta_contravencion", "text": "Presunta contravención" }, + { + "id": "privacion_ilegitima_de_la_libertad", + "text": "Privacion ilegítima de la libertad" + }, + { "id": "producir_avalanchas", "text": "Producir avalanchas" }, + { + "id": "promocion_o_facilitacion_de_prostitucion_de_personas", + "text": "Promoción o facilitación de prostitución de personas" + }, + { + "id": "promover_comerciar_ofertar_juego", + "text": "Promover comerciar ofertar juego" + }, + { + "id": "propaganda_discriminatoria", + "text": "Propaganda discriminatoria" + }, + { "id": "proteccion_animal", "text": "Protección animal" }, + { "id": "provocar_parcialidad", "text": "Provocar parcialidad" }, + { + "id": "publicidad_en_lugares_no_habilitados_via_publica", + "text": "Publicidad en lugares no habilitados via pública" + }, + { + "id": "regentear_o_administrar_casas_de_tolerancia", + "text": "Regentear o administrar casas de tolerancia" + }, + { "id": "residuos_peligrosos", "text": "Residuos peligrosos" }, + { "id": "retencion_indebida", "text": "Retención indebida" }, + { "id": "revender_entradas", "text": "Revender entradas" }, + { "id": "robo", "text": "Robo" }, + { "id": "ruidos_molestos", "text": "Ruidos molestos" }, + { "id": "ruidos_y_vibraciones", "text": "Ruidos y vibraciones" }, + { "id": "sancion_generica", "text": "Sanción generica" }, + { + "id": "servicios_de_seguridad_prohibiciones_incumplidas", + "text": "Servicios de seguridad prohibiciones incumplidas" + }, + { + "id": "servicios_de_seguridad_requisitos_incumplidos", + "text": "Servicios de seguridad requisitos incumplidos" + }, + { "id": "suministrar_alcohol_menor", "text": "Suministrar alcohol menor" }, + { + "id": "suministrar_bebidas_alcoholicas", + "text": "Suministrar bebidas alcohólicas" + }, + { + "id": "suministrar_elementos_aptos_agresion", + "text": "Suministrar elementos aptos agresión" + }, + { + "id": "suministrar_material_pornografico", + "text": "Suministrar material pornográfico" + }, + { + "id": "suministrar_objetos_peligrosos", + "text": "Suministrar objetos peligrosos" + }, + { + "id": "suministrar_productos_farmaceuticos", + "text": "Suministrar productos farmacéuticos" + }, + { + "id": "suministro_de_medicamentos", + "text": "Suministro de medicamentos" + }, + { "id": "suplantacion_identidad", "text": "Suplantación identidad" }, + { + "id": "suplantacion_digital_de_identidad", + "text": "Suplantación digital de identidad" + }, + + { + "id": "taxis_transportes_escolares_remises_sin_autorizacion", + "text": "Taxis transportes escolares remises sin autorización" + }, + { "id": "tenencia_de_arma", "text": "Tenencia de arma" }, + { "id": "transporte_de_pasajeros", "text": "Transporte de pasajeros" }, + { + "id": "transporte_de_pasajeros_sin_habilitacion", + "text": "Transporte de pasajeros sin habilitación" + }, + { + "id": "transporte_de_pasajeros_sin_habilitacion_o_autorizacion", + "text": "Transporte de pasajeros sin habilitación o autorización" + }, + { + "id": "usar_indebidamente_credencial", + "text": "Usar indebidamente credencial" + }, + { + "id": "usar_indebidamente_espacio_publico", + "text": "Usar indebidamente espacio público" + }, + { "id": "usar_indebidamente_armas", "text": "Usar indebidamente armas" }, + { + "id": "uso_de_documento_falso_o_adulterado", + "text": "Uso de documento falso o adulterado" + }, + { + "id": "uso_indebido_del_espacio_publico", + "text": "Uso indebido del espacio público" + }, + { + "id": "uso_o_exhibicion_de_franquicias", + "text": "Uso o exhibición de franquicias" + }, + { "id": "usurpacion", "text": "Usurpación" }, + { "id": "vehiculo_abandonado", "text": "Vehiculo abandonado" }, + { + "id": "vender_entradas_o_permitir_ingreso_exceso", + "text": "Vender entradas o permitir ingreso exceso" + }, + { + "id": "vender_sustancias_medicinales_sin_receta", + "text": "Vender sustancias medicinales sin receta" + }, + { + "id": "venta_o_consumo_de_bebidas_alcoholicas_fuera_del_horario", + "text": "Venta o consumo de bebidas alcohólicas fuera del horario" + }, + { "id": "verificacion_tecnica", "text": "Verificación técnica" }, + { "id": "violacion_de_peaje", "text": "Violación de peaje" }, + { "id": "violacion_de_domicilio", "text": "Violación de domicilio" }, + { + "id": "violacion_de_secretos_y_de_la_privacidad", + "text": "Violación de secretos y de la privacidad" + }, + { "id": "violar_clausura", "text": "Violar clausura" }, + { + "id": "violar_inhabilitacion_para_conducir", + "text": "Violar inhabilitación para conducir" + }, + { + "id": "violar_reglamentacion_juego", + "text": "Violar reglamentación juego" + }, + { + "id": "zanjas_y_pozos_en_via_publica", + "text": "Zanjas y pozos en via pública" + } + ], + "CONDUCTA_DESCRIPCION": [ + { "id": "agravada", "text": "Agravada" }, + { "id": "agravadas_por_edad", "text": "Agravadas por edad" }, + { + "id": "agravadas_por_el_uso_de_armas", + "text": "Agravadas por el uso de armas" + }, + { + "id": "agravadas_por_uso_de_armas", + "text": "Agravadas por uso de armas" + }, + { + "id": "agravadas_por_uso_de_armas_impropia", + "text": "Agravadas por uso de armas impropia" + }, + { "id": "agravado", "text": "Agravado" }, + { "id": "agravado_abuso_funcion", "text": "Agravado abuso función" }, + { "id": "agravado_alevosia", "text": "Agravado alevosía" }, + { + "id": "agravado_causar_sufrimiento_otra_persona", + "text": "Agravado causar sufrimiento otra persona" + }, + { "id": "agravado_concurso", "text": "Agravado concurso" }, + { "id": "agravado_familiar", "text": "Agravado familiar" }, + { + "id": "agravado_insertar_datos_en_datos_personales", + "text": "Agravado insertar datos en datos personales" + }, + { "id": "agravado_lugar_publico", "text": "Agravado lugar público" }, + { "id": "agravado_mas_personas", "text": "Agravado más personas" }, + { "id": "agravado_medio_idoneo", "text": "Agravado medio idóneo" }, + { "id": "agravado_menor_de_edad", "text": "Agravado menor de edad" }, + { "id": "agravado_miembro_fuerzas", "text": "Agravado miembro fuerzas" }, + { "id": "agravado_monumentos", "text": "Agravado monumentos" }, + { "id": "agravado_otro_delito", "text": "Agravado otro delito" }, + { + "id": "agravado_peligro_de_muerte", + "text": "Agravado peligro de muerte" + }, + { "id": "agravado_por_el_vinculo", "text": "Agravado por el vínculo" }, + { "id": "agravado_por_espacio", "text": "Agravado por espacio" }, + { + "id": "agravado_por_ser_fuerza_de_seguridad", + "text": "Agravado por ser fuerza de seguridad" + }, + { "id": "agravado_precio", "text": "Agravado precio" }, + { + "id": "agravado_relacion_de_pareja", + "text": "Agravado relación de pareja" + }, + { "id": "agravado_superior_militar", "text": "Agravado superior militar" }, + { + "id": "agravado_violar_informacion_banco_de_datos", + "text": "Agravado violar información banco de datos" + }, + { + "id": "agravado_violar_sistemas_seguridad", + "text": "Agravado violar sistemas seguridad" + }, + { + "id": "agravado_violencia_de_genero", + "text": "Agravado violencia de género" + }, + { "id": "agravado_violencia_mujer", "text": "Agravado violencia mujer" }, + { "id": "agravados", "text": "Agravados" }, + { "id": "coactivas", "text": "Coactivas" }, + { "id": "coactivas_agravadas", "text": "Coactivas agravadas" }, + { + "id": "coactivas_agravadas_por_uso_de_armas", + "text": "Coactivas agravadas por uso de armas" + }, + { + "id": "comercio_de_plantas_para_producir_estupefacientes", + "text": "Comercio de plantas para producir estupefacientes" + }, + { "id": "con_escalamiento", "text": "Con escalamiento" }, + { "id": "conduccion_imprudente", "text": "Conducción imprudente" }, + { "id": "culposas", "text": "Culposas" }, + { "id": "culposo", "text": "Culposo" }, + { "id": "de_fuego_uso_civil", "text": "De fuego uso civil" }, + { "id": "de_guerra", "text": "De guerra" }, + { + "id": "destinado_a_acreditar_identidad_de_personas_habilitacion_o_titularidad", + "text": "Destinado a acreditar identidad de personas habilitación o titularidad" + }, + { "id": "digital", "text": "Digital" }, + { "id": "digital_agravado_familiar", "text": "Digital agravado familiar" }, + { "id": "digital_agravado_jefe", "text": "Digital agravado jefe" }, + { + "id": "digital_agravado_mas_de_una_persona", + "text": "Digital agravado más de una persona" + }, + { + "id": "digital_agravado_menor_de_edad", + "text": "Digital agravado menor de edad" + }, + { + "id": "digital_agravado_relacion_de_pareja", + "text": "Digital agravado relación de pareja" + }, + { + "id": "durante_espectaculo_deportivo", + "text": "Durante espectáculo deportivo" + }, + { + "id": "en_grandes_parques_o_espectaculos_masivos", + "text": "En grandes parques o espectáculos masivos" + }, + { "id": "en_riña", "text": "En riña" }, + { + "id": "entrega_suministro_facilitacion", + "text": "Entrega suministro facilitación" + }, + { "id": "funcionario_publico", "text": "Funcionario público" }, + { "id": "ganzua_llave", "text": "Ganzúa llave" }, + { "id": "graves", "text": "Graves" }, + { "id": "graves_tentativa", "text": "Graves tentativa" }, + { "id": "informatica", "text": "Informática" }, + { + "id": "ingreso_a_domicilio_sin_autorizacion", + "text": "Ingreso a domicilio sin autorización" + }, + { + "id": "intimidacion_o_invocando_orden_superior", + "text": "Intimidación o invocando orden superior" + }, + { "id": "leves", "text": "Leves" }, + { "id": "leves_agravadas", "text": "Leves agravadas" }, + { "id": "leves_tentativa", "text": "Leves tentativa" }, + { "id": "muebles_transportables", "text": "Muebles transportables" }, + { "id": "no_denuncia_delito", "text": "No denuncia delito" }, + { "id": "no_corresponde", "text": "No corresponde" }, + { "id": "ocasion_de_incendio", "text": "Ocasión de incendio" }, + { "id": "organismo_publico", "text": "Organismo público" }, + { + "id": "para_produccion_o_tenencia_con_fines_de_comercializacion", + "text": "Para producción o tenencia con fines de comercialización" + }, + { "id": "por_despojo", "text": "Por despojo" }, + { + "id": "por_obligacion_de_devolver", + "text": "Por obligación de devolver" + }, + { + "id": "por_turbacion_de_la_posesion", + "text": "Por turbación de la posesión" + }, + { "id": "productos_separados_suelo", "text": "Productos separados suelo" }, + { "id": "publico_o_privado", "text": "Público o privado" }, + { "id": "seguido_de_muerte", "text": "Seguido de muerte" }, + { "id": "simple", "text": "Simple" }, + { "id": "simples", "text": "Simples" }, + { "id": "sin_autorizacion", "text": "Sin autorización" }, + { "id": "tarjeta_credito_debito", "text": "Tarjeta crédito débito" }, + { "id": "tenencia", "text": "Tenencia" }, + { "id": "tentativa", "text": "Tentativa" }, + { + "id": "tentativa_agravado_alevosia", + "text": "Tentativa agravado alevosía" + }, + { + "id": "transporte_de_pasajeros_sin_habilitacion_o_autorizacion", + "text": "Transporte de pasajeros sin habilitación o autorización" + }, + { "id": "uso_de_tarjeta_o_datos", "text": "Uso de tarjeta o datos" }, + { "id": "vehiculos", "text": "Vehículos" } + ], + "DETALLE": [ + { + "id": "audiencia_victima_ley_26484", + "text": "Audiencia víctima Ley 26484" + }, + { + "id": "audiencia_victima_11_bis_ley_24660", + "text": "Audiencia víctima 11 bis Ley 24660" + }, + { + "id": "audiencia_victima_ley_26485", + "text": "Audiencia victima Ley 26485" + }, + { + "id": "audiencia_acusado_ley_26485", + "text": "Audiencia acusado Ley 26485" + }, + { "id": "29_dias", "text": "29 días" }, + { "id": "30_dias", "text": "30 dias" }, + { "id": "44_dias", "text": "44 días" }, + { "id": "45_dias", "text": "45 dias" }, + { "id": "59_dias", "text": "59 días" }, + { "id": "60_dias", "text": "60 dias" }, + { "id": "89_dias", "text": "89 días" }, + { "id": "90_dias", "text": "90 dias" }, + { "id": "179_dias", "text": "179 días" }, + { "id": "180_dias", "text": "180 dias" }, + { "id": "abandono_vehiculo", "text": "Abandono vehículo" }, + { "id": "absolucion", "text": "Absolución" }, + { + "id": "absolucion_desistimiento_fiscal", + "text": "Absolución desistimiento fiscal" + }, + { "id": "accesorias_legales", "text": "Accesorias legales" }, + { + "id": "acepta_competencia_inhibitoria", + "text": "Acepta competencia inhibitoria" + }, + { "id": "acepta_cuestion_turno", "text": "Acepta cuestión turno" }, + { "id": "aclaratoria", "text": "Aclaratoria" }, + { "id": "acta_de_comprobacion", "text": "Acta de comprobación" }, + { "id": "acta_de_intimacion", "text": "Acta de intimación" }, + { + "id": "acuerdo_de_pago_levanta_embargo", + "text": "Acuerdo de pago levanta embargo" + }, + { + "id": "acumulacion_por_conexidad_y_ampliacion", + "text": "Acumulación por conexidad y ampliación" + }, + { "id": "admisibilidad", "text": "Admisibilidad" }, + { "id": "agente_encubierto", "text": "Agente encubierto" }, + { "id": "agente_revelador", "text": "Agente revelador" }, + { "id": "agotamiento_de_la_pena", "text": "Agotamiento de la pena" }, + { "id": "alimentos_provisorios", "text": "Alimentos provisorios" }, + { "id": "allanamiento", "text": "Allanamiento" }, + { "id": "apelacion", "text": "Apelación" }, + { + "id": "apertura_extraccion_examen_datos", + "text": "Apertura extracción examen datos" + }, + { "id": "aprehension", "text": "Aprehensión" }, + { "id": "arma", "text": "Arma" }, + { "id": "armas", "text": "Armas" }, + { + "id": "arresto_domiciliario_deja_sin_efecto", + "text": "Arresto domiciliario deja sin efecto" + }, + { "id": "asesor_tutelar", "text": "Asesor tutelar" }, + { "id": "atipicidad_sobreseimiento", "text": "Atipicidad sobreseimiento" }, + { + "id": "audiencia_acusado_ley_26484", + "text": "Audiencia acusado Ley 26484" + }, + { + "id": "audiencia_acusado_difiere_sentencia", + "text": "Audiencia acusado difiere sentencia" + }, + { "id": "audiencia_preliminar", "text": "Audiencia preliminar" }, + { + "id": "audiencia_victima_10_bis_ley_24660", + "text": "Audiencia víctima 10 bis Ley 24660" + }, + { "id": "bloqueo_acceso_al_dominio", "text": "Bloqueo acceso al dominio" }, + { + "id": "cadena_de_custodia_de_elementos_secuestrados", + "text": "Cadena de custodia de elementos secuestrados" + }, + { "id": "camara_gesell", "text": "Cámara Gesell" }, + { + "id": "cambio_domicilio_prision_domiciliaria", + "text": "Cambio domicilio prisión domiciliaria" + }, + { "id": "captura", "text": "Captura" }, + { "id": "captura_deja_sin_efecto", "text": "Captura deja sin efecto" }, + { "id": "caucion_real", "text": "Caución real" }, + { "id": "cedula_de_notificacion", "text": "Cédula de notificación" }, + { + "id": "cese_de_actos_de_perturbacion_o_intimidacion", + "text": "Cese de actos de perturbación o intimidación" + }, + { "id": "cese_de_medidas", "text": "Cese de medidas" }, + { "id": "cese_de_medidas_parcial", "text": "Cese de medidas parcial" }, + { "id": "cierre_de_instancia", "text": "Cierre de instancia" }, + { "id": "clausura", "text": "Clausura" }, + { "id": "clausura_deja_sin_efecto", "text": "Clausura deja sin efecto" }, + { "id": "competencia_penal_juvenil", "text": "Competencia penal juvenil" }, + { "id": "concede", "text": "Concede" }, + { "id": "concede_parcialmente", "text": "Concede parcialmente" }, + { "id": "conocimiento_personal", "text": "Conocimiento personal" }, + { "id": "consigna_policial", "text": "Consigna policial" }, + { "id": "contienda_negativa", "text": "Contienda negativa" }, + { "id": "control", "text": "Control" }, + { "id": "convalida_fallecimiento", "text": "Convalida fallecimiento" }, + { "id": "convalida_inimputabilidad", "text": "Convalida inimputabilidad" }, + { + "id": "convalidacion_otras_causales", + "text": "Convalidación otras causales" + }, + { "id": "copia_forense_celular", "text": "Copia forense celular" }, + { + "id": "copias_de_capturas_de_pantalla_mensajes_y_llamados", + "text": "Copias de capturas de pantalla mensajes y llamados" + }, + { + "id": "cosa_juzgada_sobreseimiento", + "text": "Cosa juzgada sobreseimiento" + }, + { "id": "cuarto_intermedio", "text": "Cuarto intermedio" }, + { + "id": "cuarto_intermedio_para_resolver", + "text": "Cuarto intermedio para resolver" + }, + { + "id": "cumplimiento_de_regla_de_conducta_en_complejo_penitenciario", + "text": "Cumplimiento de regla de conducta en complejo penitenciario" + }, + { + "id": "cumplimiento_de_reglas_de_conducta", + "text": "Cumplimiento de reglas de conducta" + }, + { "id": "declaracion_testimonial", "text": "Declaracion testimonial" }, + { "id": "declina_competencia", "text": "Declina competencia" }, + { "id": "defensa", "text": "Defensa" }, + { "id": "defensa_particular", "text": "Defensa particular" }, + { "id": "defensa_querella", "text": "Defensa querella" }, + { + "id": "deja_sin_efecto_inhabilitacion_y_devuelve_licencia", + "text": "Deja sin efecto inhabilitación y devuelve licencia" + }, + { + "id": "deja_sin_efecto_revocacion", + "text": "Deja sin efecto revocación" + }, + { "id": "delitos_de_accion_publica", "text": "Delitos de acción publica" }, + { + "id": "desistimiento_accion_instancia_privada_sobreseimiento", + "text": "Desistimiento accion instancia privada sobreseimiento" + }, + { "id": "desistimiento_fiscal", "text": "Desistimiento fiscal" }, + { "id": "destruccion_de_arma", "text": "Destrucción de arma" }, + { "id": "detencion", "text": "Detención" }, + { + "id": "detencion_captura_y/o_traslado", + "text": "Detención captura y/o traslado" + }, + { + "id": "detencion_captura_y/o_traslado_deja_sin_efecto", + "text": "Detención captura y/o traslado deja sin efecto" + }, + { + "id": "detencion_secuestro_y_requisa", + "text": "Detención secuestro y requisa" + }, + { + "id": "devolucion_acarreo_y_estadia", + "text": "Devolución acarreo y estadía" + }, + { "id": "dictamen_fiscal", "text": "Dictamen fiscal" }, + { "id": "difiere_decision", "text": "Difiere decisión" }, + { "id": "difiere_fundamentos", "text": "Difiere fundamentos" }, + { "id": "difiere_resolucion", "text": "Difiere resolución" }, + { + "id": "dispositivo_de_geoposicionamiento_mantiene_hasta_juicio", + "text": "Dispositivo de geoposicionamiento mantiene hasta juicio" + }, + { + "id": "domiciliaria_con_vigilancia_electronica", + "text": "Domiciliaria con vigilancia electrónica" + }, + { "id": "egreso_del_pais", "text": "Egreso del país" }, + { "id": "ejecucion_de_honorarios", "text": "Ejecución de honorarios" }, + { "id": "embargo", "text": "Embargo" }, + { "id": "embargo_deja_sin_efecto", "text": "Embargo deja sin efecto" }, + { "id": "estimulo_educativo", "text": "Estímulo educativo" }, + { "id": "estupefacientes", "text": "Estupefacientes" }, + { "id": "estupefacientes_dinero", "text": "Estupefacientes dinero" }, + { + "id": "estupefacientes_telefono_dinero", + "text": "Estupefacientes teléfono dinero" + }, + { + "id": "estupefacientes_telefono_dinero_arma", + "text": "Estupefacientes teléfono dinero arma" + }, + { + "id": "estupefacientes_telefono_vehiculo", + "text": "Estupefacientes teléfono vehículo" + }, + { + "id": "estupefacientes_vehiculo_dinero", + "text": "Estupefacientes vehículo dinero" + }, + { "id": "excarcelacion", "text": "Excarcelación" }, + { "id": "exclusion_del_hogar", "text": "Exclusión del hogar" }, + { "id": "exhorto", "text": "Exhorto" }, + { "id": "exhorto_diplomatico", "text": "Exhorto diplomático" }, + { "id": "exime_pago_multa", "text": "Exime pago multa" }, + { + "id": "exime_reparacion_del_daño_y_pago_multa", + "text": "Exime reparación del daño y pago multa" + }, + { "id": "eximicion_pago", "text": "Eximición pago" }, + { + "id": "eximicion_pauta_de_conducta", + "text": "Eximición pauta de conducta" + }, + { + "id": "expulsion_art_63_a_ley_25871", + "text": "Expulsión art. 63 a Ley 25871" + }, + { + "id": "expulsion_art_64_a_ley_25871", + "text": "Expulsion art. 64 a Ley 25871" + }, + { + "id": "extincion_accion_por_autocomposicion", + "text": "Extinción acción por autocomposición" + }, + { + "id": "extincion_accion_por_cumplimiento", + "text": "Extinción acción por cumplimiento" + }, + { + "id": "extincion_accion_sobreseimiento", + "text": "Extinción acción sobreseimiento" + }, + { + "id": "extincion_accion_sobreseimiento_por_fallecimiento", + "text": "Extinción acción sobreseimiento por fallecimiento" + }, + { "id": "extincion_de_la_pena", "text": "Extinción de la pena" }, + { + "id": "extincion_sancion_por_cumplimiento", + "text": "Extinción sanción por cumplimiento" + }, + { "id": "extrañamiento", "text": "Extrañamiento" }, + { + "id": "falta_de_accion_sobreseimiento", + "text": "Falta de acción sobreseimiento" + }, + { + "id": "falta_de_legitimacion_pasiva", + "text": "Falta de legitimación pasiva" + }, + { "id": "falta_de_participacion", "text": "Falta de participación" }, + { "id": "falta_de_presentacion", "text": "Falta de presentación" }, + { + "id": "falta_de_requisitos_procedencia", + "text": "Falta de requisitos procedencia" + }, + { "id": "fija_nueva_audiencia", "text": "Fija nueva audiencia" }, + { "id": "fondo_de_reserva", "text": "Fondo de reserva" }, + { "id": "habeas_corpus", "text": "Habeas corpus" }, + { + "id": "hasta_agotar_investigacion", + "text": "Hasta agotar investigación" + }, + { "id": "identidad_objeto_y_sujeto", "text": "Identidad objeto y sujeto" }, + { "id": "imagenes_videos", "text": "Imagenes videos" }, + { "id": "incompetencia_material", "text": "Incompetencia material" }, + { + "id": "incompetencia_por_conexidad", + "text": "Incompetencia por conexidad" + }, + { + "id": "incompetencia_por_conexidad_subjetiva", + "text": "Incompetencia por conexidad subjetiva" + }, + { "id": "incompetencia_territorial", "text": "Incompetencia territorial" }, + { + "id": "indeterminacion_imputacion", + "text": "Indeterminacion imputación" + }, + { + "id": "informacion_y_apertura_de_antenas_llamadas", + "text": "Información y apertura de antenas llamadas" + }, + { "id": "informe_cij", "text": "Informe cij" }, + { "id": "informe_mercado_libre", "text": "Informe mercado libre" }, + { "id": "informe_ncmec", "text": "Informe ncmec" }, + { "id": "informe_previo", "text": "Informe previo" }, + { + "id": "informes_llamadas_y_deteccion_de_celdas", + "text": "Informes llamadas y detección de celdas" + }, + { + "id": "inhabilitacion_para_conducir", + "text": "Inhabilitación para conducir" + }, + { + "id": "inhabilitacion_para_conducir_deja_sin_efecto", + "text": "Inhabilitación para conducir deja sin efecto" + }, + { "id": "inmovilizacion", "text": "Inmovilización" }, + { "id": "interes_en_el_caso", "text": "Interés en el caso" }, + { "id": "internacion_involuntaria", "text": "Internación involuntaria" }, + { "id": "interprete", "text": "Intérprete" }, + { "id": "interprete_y_defensa", "text": "Intérprete y defensa" }, + { "id": "interrogatorio_policial", "text": "Interrogatorio policial" }, + { + "id": "intervencion_equipo_especializado", + "text": "Intervención equipo especializado" + }, + { "id": "intervencion_previa", "text": "Intervención previa" }, + { "id": "intervencion_red_social", "text": "Intervención red social" }, + { "id": "intervencion_telefonica", "text": "Intervención telefónica" }, + { + "id": "intervencion_telefonica_con_ubicacion_de_celda_de_apertura", + "text": "Intervención telefónica con ubicación de celda de apertura" + }, + { + "id": "levantamiento_clausura_administrativa", + "text": "Levantamiento clausura administrativa" + }, + { + "id": "levantamiento_secreto_bancario", + "text": "Levantamiento secreto bancario" + }, + { + "id": "levantamiento_secreto_fiscal", + "text": "Levantamiento secreto fiscal" + }, + { "id": "liberacion_aves", "text": "Liberación aves" }, + { + "id": "libertad_asistida_incorporacion", + "text": "Libertad asistida incorporación" + }, + { + "id": "libertad_condicional_otorga", + "text": "Libertad condicional otorga" + }, + { + "id": "libertad_condicional_revoca", + "text": "Libertad condicional revoca" + }, + { "id": "litispendencia", "text": "Litispendencia" }, + { "id": "mantiene", "text": "Mantiene" }, + { "id": "mediacion", "text": "Mediación" }, + { "id": "medidas_restrictivas", "text": "Medidas restrictivas" }, + { + "id": "modifica_pauta_de_conducta", + "text": "Modifica pauta de conducta" + }, + { "id": "morigeracion_domiciliaria", "text": "Morigeración domiciliaria" }, + { "id": "no_convalida", "text": "No convalida" }, + { "id": "no_homologa_absolucion", "text": "No homologa absolución" }, + { "id": "notificacion_via_mail", "text": "Notificacion vía mail" }, + { + "id": "nulidad_incorporacion_prueba", + "text": "Nulidad incorporación prueba" + }, + { "id": "otorga_plazo", "text": "Otorga plazo" }, + { "id": "otras_vias_idoneas", "text": "Otras vías idóneas" }, + { "id": "pago_de_multa_en_cuotas", "text": "Pago de multa en cuotas" }, + { + "id": "pago_minimo_multa_sobreseimiento", + "text": "Pago mínimo multa sobreseimiento" + }, + { + "id": "pago_minimo_multa_y_reparacion_daño_63cp", + "text": "Pago mínimo multa y reparación daño 63 CP" + }, + { + "id": "pago_minimo_multa_y_reparacion_daño_64cp", + "text": "Pago minimo multa y reparacion daño 64 CP" + }, + { "id": "paradero", "text": "Paradero" }, + { "id": "paradero_deja_sin_efecto", "text": "Paradero deja sin efecto" }, + { + "id": "parcial_requerimiento_de_juicio", + "text": "Parcial requerimiento de juicio" + }, + { + "id": "parcial_requerimiento_de_juicio_falta_de_circunscripcion_temporal", + "text": "Parcial requerimiento de juicio falta de circunscripción temporal" + }, + { + "id": "parcial_requerimiento_de_juicio_falta_de_fundamentacion", + "text": "Parcial requerimiento de juicio falta de fundamentación" + }, + { + "id": "parcial_requerimiento_de_juicio_por_introduccion_de_agravante", + "text": "Parcial requerimiento de juicio por introducción de agravante" + }, + { "id": "pedido_de_informe_banco", "text": "Pedido de informe banco" }, + { + "id": "pedido_de_informe_facebook", + "text": "Pedido de informe Facebook" + }, + { + "id": "pedido_de_informe_facebook_google", + "text": "Pedido de informe Facebook Google" + }, + { + "id": "pedido_de_informe_facebook_microsoft", + "text": "Pedido de informe Facebook Microsoft" + }, + { "id": "pedido_de_informe_google", "text": "Pedido de informe Google" }, + { + "id": "pedido_de_informe_google_microsoft", + "text": "Pedido de informe Google Microsoft" + }, + { + "id": "pedido_de_informe_imgur_llc", + "text": "Pedido de informe Imgur LLC" + }, + { + "id": "pedido_de_informe_instagram", + "text": "Pedido de informe Instagram" + }, + { + "id": "pedido_de_informe_mercado_libre", + "text": "Pedido de informe Mercado Libre" + }, + { + "id": "pedido_de_informe_microsoft", + "text": "Pedido de informe Microsoft" + }, + { "id": "pedido_de_informe_skype", "text": "Pedido de informe Skype" }, + { + "id": "pedido_de_informe_telefonia", + "text": "Pedido de informe telefonía" + }, + { + "id": "pedido_de_informe_whatsapp", + "text": "Pedido de informe Whatsapp" + }, + { "id": "pedido_informe_tiktok", "text": "Pedido informe Tiktok" }, + + { + "id": "pedido_prision_domiciliaria", + "text": "Pedido prisión domiciliaria" + }, + { + "id": "pena_de_arresto_efectivo_cumplimiento", + "text": "Pena de arresto efectivo cumplimiento" + }, + { + "id": "pena_de_arresto_efectivo_cumplimiento_compurgada", + "text": "Pena de arresto efectivo cumplimiento compurgada" + }, + { + "id": "pena_de_arresto_en_suspenso", + "text": "Pena de arresto en suspenso" + }, + { + "id": "pena_de_inhabilitacion_especial", + "text": "Pena de inhabilitación especial" + }, + { + "id": "pena_de_multa_efectivo_cumplimiento", + "text": "Pena de multa efectivo cumplimiento" + }, + { "id": "pena_de_multa_en_suspenso", "text": "Pena de multa en suspenso" }, + { + "id": "pena_de_multa_sustituida_por_amonestacion", + "text": "Pena de multa sustituida por amonestación" + }, + { + "id": "pena_de_multa_sustituida_por_clausura_se_tiene_por_cumplida", + "text": "Pena de multa sustituida por clausura se tiene por cumplida" + }, + { + "id": "pena_de_multa_sustituida_por_trabajos_comunitarios", + "text": "Pena de multa sustituida por trabajos comunitarios" + }, + { + "id": "pena_de_prision_domiciliaria", + "text": "Pena de prisión domiciliaria" + }, + { + "id": "pena_de_prision_efectivo_cumplimiento", + "text": "Pena de prisión efectivo cumplimiento" + }, + { + "id": "pena_de_prision_efectivo_cumplimiento_compurgada", + "text": "Pena de prisión efectivo cumplimiento compurgada" + }, + { + "id": "pena_de_prision_efectivo_cumplimiento_domiciliaria", + "text": "Pena de prisión efectivo cumplimiento domiciliaria" + }, + { + "id": "pena_de_prision_en_suspenso", + "text": "Pena de prisión en suspenso" + }, + { + "id": "pena_de_tareas_comunitarias", + "text": "Pena de tareas comunitarias" + }, + { + "id": "pena_de_tareas_de_utilidad_publica", + "text": "Pena de tareas de utilidad pública" + }, + { "id": "pericia", "text": "Pericia" }, + { "id": "pericia_arma", "text": "Pericia arma" }, + { + "id": "pericia_examen_veterinario", + "text": "Pericia examen veterinario" + }, + { "id": "peritaje_de_telefono", "text": "Peritaje de teléfono" }, + { + "id": "peritaje_ordenadores_y_o_telefonos", + "text": "Peritaje ordenadores y o teléfonos" + }, + { "id": "peritaje_telefono", "text": "Peritaje teléfono" }, + { "id": "perito", "text": "Perito" }, + { "id": "permiso_de_viaje", "text": "Permiso de viaje" }, + { + "id": "plazo_razonable_sobreseimiento", + "text": "Plazo razonable sobreseimiento" + }, + { + "id": "plazo_razonable_sobresimiento", + "text": "Plazo razonable sobresimiento" + }, + { "id": "pleito_con_una_parte", "text": "Pleito con una parte" }, + { + "id": "por_quince_dias_hasta_tomar_contacto_con_victima", + "text": "Por quince dias hasta tomar contacto con víctima" + }, + { + "id": "prescripcion_sobreseimiento", + "text": "Prescripción sobreseimiento" + }, + { + "id": "prision_domiciliaria_mantiene", + "text": "Prisión domiciliaria mantiene" + }, + { + "id": "prision_domiciliaria_prorroga", + "text": "Prisión domiciliaria prórroga" + }, + { "id": "procedimiento", "text": "Procedimiento" }, + { + "id": "procedimiento_administrativo_pendiente", + "text": "Procedimiento administrativo pendiente" + }, + { + "id": "prohibicion_compra_tenencia_registracion_de_armas", + "text": "Prohibición compra tenencia registración de armas" + }, + { + "id": "prohibicion_de_acercamiento", + "text": "Prohibición de acercamiento" + }, + { + "id": "prohibicion_de_acercamiento_y_contacto", + "text": "Prohibición de acercamiento y contacto" + }, + { + "id": "prohibicion_de_publicar_en_redes", + "text": "Prohibición de publicar en redes" + }, + { + "id": "prohibicion_de_publicar_en_redes_sobre", + "text": "Prohibición de publicar en redes sobre" + }, + { + "id": "promocion_a_periodo_de_prueba_art_6_ley_24660", + "text": "Promoción a período de prueba art 6 Ley 24660" + }, + { + "id": "promocion_a_periodo_de_prueba_art_7_ley_24660", + "text": "Promoción a período de prueba art 7 Ley 24660" + }, + { "id": "prorroga", "text": "Prórroga" }, + { "id": "prueba_ofrecida", "text": "Prueba ofrecida" }, + { "id": "publicacion_de_edictos", "text": "Publicación de edictos" }, + { "id": "queja", "text": "Queja" }, + { "id": "querella", "text": "Querella" }, + { + "id": "quita_puntos_de_licencia_de_conducir", + "text": "Quita puntos de licencia de conducir" + }, + { "id": "realizacion", "text": "Realización" }, + { "id": "rebeldia", "text": "Rebeldía" }, + { "id": "rebeldia_deja_sin_efecto", "text": "Rebeldía deja sin efecto" }, + { "id": "rebeldia_y_captura", "text": "Rebeldía y captura" }, + { "id": "rebeldia_y_paradero", "text": "Rebeldía y paradero" }, + { "id": "rechaza_acuerdo", "text": "Rechaza acuerdo" }, + { "id": "rechaza_competencia", "text": "Rechaza competencia" }, + { + "id": "regimen_de_comunicacion_provisoria", + "text": "Regimen de comunicación provisoria" + }, + { "id": "reincidencia", "text": "Reincidencia" }, + { + "id": "reintegra_puntos_de_licencia_de_conducir", + "text": "Reintegra puntos de licencia de conducir" + }, + { "id": "remate_vehiculo", "text": "Remate vehículo" }, + { "id": "remision_a_justicia_civil", "text": "Remisión a justicia civil" }, + { + "id": "remision_camara_para_sorteo", + "text": "Remisión cámara para sorteo" + }, + { + "id": "reparacion_integral_del_daño_sobreseimiento", + "text": "Reparación integral del daño sobreseimiento" + }, + { + "id": "replica_de_arma_de_plastico", + "text": "Réplica de arma de plástico" + }, + { "id": "reposicion", "text": "Reposición" }, + { + "id": "reposicion_apelacion_subsidio", + "text": "Reposición apelación subsidio" + }, + { "id": "reprogramacion", "text": "Reprogramación" }, + { "id": "requerido_por_infractor", "text": "Requerido por infractor" }, + { "id": "requerimiento_de_juicio", "text": "Requerimiento de juicio" }, + { + "id": "requerimiento_de_juicio_falta_de_fundamentacion", + "text": "Requerimiento de juicio falta de fundamentación" + }, + { "id": "requisa", "text": "Requisa" }, + { "id": "restitucion_de_inmueble", "text": "Restitución de inmueble" }, + { + "id": "revision_psicofisica_capacidad", + "text": "Revisión psicofísica capacidad" + }, + { + "id": "revision_psiquiatrica_y_psicologica", + "text": "Revision psiquiátrica y psicológica" + }, + { "id": "revoca", "text": "Revoca" }, + { "id": "revoca_condicionalidad", "text": "Revoca condicionalidad" }, + { "id": "revocacion", "text": "Revocación" }, + { "id": "rueda_de_reconocimiento", "text": "Rueda de reconocimiento" }, + { "id": "salidas_extraordinarias", "text": "Salidas extraordinarias" }, + { "id": "salidas_transitorias", "text": "Salidas transitorias" }, + { + "id": "sancion_sustitutiva_prorroga", + "text": "Sanción sustitutiva prórroga" + }, + { "id": "secuestro", "text": "Secuestro" }, + { "id": "sin_indicacion_plazo", "text": "Sin indicación plazo" }, + { "id": "sobreseimiento", "text": "Sobreseimiento" }, + { "id": "solicita", "text": "Solicita" }, + { + "id": "solicitud_destruccion_de_estupefacientes", + "text": "Solicitud destrucción de estupefacientes" + }, + { "id": "tareas_de_investigacion", "text": "Tareas de investigación" }, + { "id": "telefono", "text": "Teléfono" }, + { + "id": "temor_parcialidad_generico", + "text": "Temor parcialidad genérico" + }, + { "id": "tiene_por_desistido", "text": "Tiene por desistido" }, + { + "id": "tiene_por_no_computado_plazo", + "text": "Tiene por no computado plazo" + }, + { "id": "tiene_presente", "text": "Tiene presente" }, + { "id": "tiene_presente_acuerdo", "text": "Tiene presente acuerdo" }, + { "id": "tobillera_refractaria", "text": "Tobillera refractaria" }, + { + "id": "traslado_por_fuerza_publica", + "text": "Traslado por fuerza pública" + }, + { + "id": "unifica_solicita_inhibitoria", + "text": "Unifica solicita inhibitoria" + }, + { "id": "unificacion_de_pena", "text": "Unificación de pena" }, + { "id": "vehiculo", "text": "Vehículo" }, + { "id": "vencimiento_plazo_ipp", "text": "Vencimiento plazo IPP" } + ], + "OBJETO_DE_LA_RESOLUCION": [ + { "id": "acumulacion_de_caso", "text": "Acumulación de caso" }, + { "id": "admisibilidad_amparo", "text": "Admisibilidad amparo" }, + { + "id": "admisibilidad_habeas_corpus", + "text": "Admisibilidad habeas corpus" + }, + { "id": "admisibilidad_prueba", "text": "Admisibilidad prueba" }, + { "id": "apartamiento", "text": "Apartamiento" }, + { "id": "archivo_fiscal", "text": "Archivo fiscal" }, + { "id": "audiencia_de_conciliacion", "text": "Audiencia de conciliación" }, + { + "id": "beneficio_litigar_sin_gastos", + "text": "Beneficio litigar sin gastos" + }, + { "id": "caducidad_de_instancia", "text": "Caducidad de instancia" }, + { "id": "cuestion_de_competencia", "text": "Cuestión de competencia" }, + { "id": "decomiso", "text": "Decomiso" }, + { "id": "desistimiento_faltas", "text": "Desistimiento faltas" }, + { "id": "ejecucion_de_la_pena", "text": "Ejecución de la pena" }, + { "id": "excepcion", "text": "Excepción" }, + { "id": "excusacion", "text": "Excusación" }, + { "id": "extraccion_testimonios", "text": "Extracción testimonios" }, + { "id": "juicio_abreviado", "text": "Juicio abreviado" }, + { "id": "juicio_oral", "text": "Juicio oral" }, + { "id": "juzgamiento_faltas", "text": "Juzgamiento faltas" }, + { "id": "mediacion", "text": "Mediación" }, + { "id": "medida_cautelar", "text": "Medida cautelar" }, + { "id": "medida_probatoria", "text": "Medida probatoria" }, + { "id": "mesa_de_dialogo", "text": "Mesa de diálogo" }, + { "id": "nulidad", "text": "Nulidad" }, + { "id": "prision_preventiva", "text": "Prisión preventiva" }, + { + "id": "prorroga_investigacion_penal_preparatoria", + "text": "Prórroga investigación penal preparatoria" + }, + { "id": "recurso", "text": "Recurso" }, + { "id": "recusacion", "text": "Recusación" }, + { "id": "regulacion_de_honorarios", "text": "Regulación de honorarios" }, + { "id": "remision_controladora", "text": "Remisión controladora" }, + { + "id": "restablecimiento_de_contacto", + "text": "Restablecimiento de contacto" + }, + { "id": "restitucion_de_efectos", "text": "Restitución de efectos" }, + { "id": "restitucion_de_inmueble", "text": "Restitución de inmueble" }, + { "id": "restitucion_efectos", "text": "Restitución efectos" }, + { + "id": "suspension_del_proceso_a_prueba", + "text": "Suspensión del proceso a prueba" + } + ], + "ART_INFRINGIDO": [ + { "id": "00", "text": "00" }, + { "id": "1", "text": "1" }, + { "id": "1.1.6", "text": "1.1.6" }, + { "id": "1.3.20", "text": "1.3.20" }, + { "id": "1.3.22", "text": "1.3.22" }, + { "id": "1.3.3", "text": "1.3.3" }, + { "id": "1.3.31", "text": "1.3.31" }, + { "id": "1.3.6.1", "text": "1.3.6.1" }, + { "id": "2.1.1", "text": "2.1.1" }, + { "id": "2.1.13", "text": "2.1.13" }, + { "id": "2.1.15", "text": "2.1.15" }, + { "id": "2.1.16", "text": "2.1.16" }, + { "id": "2.1.17", "text": "2.1.17" }, + { "id": "2.1.19", "text": "2.1.19" }, + { "id": "2.1.21", "text": "2.1.21" }, + { "id": "2.1.3", "text": "2.1.3" }, + { "id": "2.2.1", "text": "2.2.1" }, + { "id": "2.2.14", "text": "2.2.14" }, + { "id": "3", "text": "3" }, + { "id": "3.1.1", "text": "3.1.1" }, + { "id": "4.1.1", "text": "4.1.1" }, + { "id": "4.1.1.2", "text": "4.1.1.2" }, + { "id": "4.1.17", "text": "4.1.17" }, + { "id": "4.1.22", "text": "4.1.22" }, + { "id": "4.1.24", "text": "4.1.24" }, + { "id": "4.1.7", "text": "4.1.7" }, + { "id": "5c", "text": "5c" }, + { "id": "5d", "text": "5d" }, + { "id": "5e", "text": "5e" }, + { "id": "6", "text": "6" }, + { "id": "6.1.12", "text": "6.1.12" }, + { "id": "6.1.12.1", "text": "6.1.12.1" }, + { "id": "6.1.2", "text": "6.1.2" }, + { "id": "6.1.24", "text": "6.1.24" }, + { "id": "6.1.26", "text": "6.1.26" }, + { "id": "6.1.28", "text": "6.1.28" }, + { "id": "6.1.31", "text": "6.1.31" }, + { "id": "6.1.37", "text": "6.1.37" }, + { "id": "6.1.4", "text": "6.1.4" }, + { "id": "6.1.40", "text": "6.1.40" }, + { "id": "6.1.41", "text": "6.1.41" }, + { "id": "6.1.42", "text": "6.1.42" }, + { "id": "6.1.44", "text": "6.1.44" }, + { "id": "6.1.47", "text": "6.1.47" }, + { "id": "6.1.49", "text": "6.1.49" }, + { "id": "6.1.52", "text": "6.1.52" }, + { "id": "6.1.54", "text": "6.1.54" }, + { "id": "6.1.63", "text": "6.1.63" }, + { "id": "6.1.67", "text": "6.1.67" }, + { "id": "6.1.8", "text": "6.1.8" }, + { "id": "6.1.9", "text": "6.1.9" }, + { "id": "6.1.94", "text": "6.1.94" }, + { "id": "9.1.1", "text": "9.1.1" }, + { "id": "10.1.5", "text": "10.1.5" }, + { "id": "11.1.10", "text": "11.1.10" }, + { "id": "11.1.2", "text": "11.1.2" }, + { "id": "11.1.4", "text": "11.1.4" }, + { "id": "13", "text": "13" }, + { "id": "14", "text": "14" }, + { "id": "17", "text": "17" }, + { "id": "23", "text": "23" }, + { "id": "51", "text": "51" }, + { "id": "52", "text": "52" }, + { "id": "53_bis", "text": "53 bis" }, + { "id": "53_bis_inc3", "text": "53 bis inc. 3" }, + { "id": "53_bis_inc5", "text": "53 bis inc. 5" }, + { "id": "54", "text": "54" }, + { "id": "56", "text": "56" }, + { "id": "57", "text": "57" }, + { "id": "58", "text": "58" }, + { "id": "59", "text": "59" }, + { "id": "67_bis", "text": "67 bis" }, + { "id": "71_quinquies", "text": "71 quinquies" }, + { "id": "71_ter", "text": "71 ter" }, + { "id": "72", "text": "72" }, + { "id": "73", "text": "73" }, + { "id": "74", "text": "74" }, + { "id": "75", "text": "75" }, + { "id": "76", "text": "76" }, + { "id": "77", "text": "77" }, + { "id": "79", "text": "79" }, + { "id": "80", "text": "80" }, + { "id": "80_inc11", "text": "80 inc. 11" }, + { "id": "80_inc2", "text": "80 inc. 2" }, + { "id": "81", "text": "81" }, + { "id": "82", "text": "82" }, + { "id": "83", "text": "83" }, + { "id": "84", "text": "84" }, + { "id": "85", "text": "85" }, + { "id": "87", "text": "87" }, + { "id": "88", "text": "88" }, + { "id": "89", "text": "89" }, + { "id": "90", "text": "90" }, + { "id": "92", "text": "92" }, + { "id": "93", "text": "93" }, + { "id": "94", "text": "94" }, + { "id": "94_bis", "text": "94 bis" }, + { "id": "95", "text": "95" }, + { "id": "96", "text": "96" }, + { "id": "97", "text": "97" }, + { "id": "100", "text": "100" }, + { "id": "101", "text": "101" }, + { "id": "104", "text": "104" }, + { "id": "105", "text": "105" }, + { "id": "106", "text": "106" }, + { "id": "107", "text": "107" }, + { "id": "109", "text": "109" }, + { "id": "111", "text": "111" }, + { "id": "113", "text": "113" }, + { "id": "116", "text": "116" }, + { "id": "118", "text": "118" }, + { "id": "119", "text": "119" }, + { "id": "120", "text": "120" }, + { "id": "122", "text": "122" }, + { "id": "125_bis", "text": "125 bis" }, + { "id": "128", "text": "128" }, + { "id": "129", "text": "129" }, + { "id": "129_parr_2", "text": "129 párr. 2" }, + { "id": "131", "text": "131" }, + { "id": "135", "text": "135" }, + { "id": "141", "text": "141" }, + { "id": "148_bis", "text": "148 bis" }, + { "id": "149_bis", "text": "149 bis" }, + { "id": "149_bis_parr_2", "text": "149 bis párr. 2" }, + { "id": "149ter_inc2b", "text": "149 ter inc. 2b)" }, + { "id": "150", "text": "150" }, + { "id": "153", "text": "153" }, + { "id": "153_bis", "text": "153 bis" }, + { "id": "153_bis_parr_2", "text": "153 bis párr. 2" }, + { "id": "157_bis", "text": "157 bis" }, + { "id": "162", "text": "162" }, + { "id": "163", "text": "163" }, + { "id": "163_inc4", "text": "163 inc. 4" }, + { "id": "163_bis", "text": "163 bis" }, + { "id": "164", "text": "164" }, + { "id": "166", "text": "166" }, + { "id": "168", "text": "168" }, + { "id": "172", "text": "172" }, + { "id": "173_inc15", "text": "173 inc. 15" }, + { "id": "173_inc16", "text": "173 inc. 16" }, + { "id": "173_inc2", "text": "173 inc. 2" }, + { "id": "181_inc1", "text": "181 inc. 1" }, + { "id": "181_inc3", "text": "181 inc. 3" }, + { "id": "183", "text": "183" }, + { "id": "183_parr_2", "text": "183 párr. 2" }, + { "id": "184_inc5", "text": "184 inc. 5" }, + { "id": "186", "text": "186" }, + { "id": "186_inc1", "text": "186 inc. 1" }, + { "id": "186_inc4", "text": "186 inc. 4" }, + { "id": "186_inc5", "text": "186 inc. 5" }, + { "id": "189_bis", "text": "189 bis" }, + { "id": "189_bis_inc4", "text": "189 bis inc. 4" }, + { "id": "193_bis", "text": "193 bis" }, + { "id": "204_quinquies", "text": "204 quinquies" }, + { "id": "205", "text": "205" }, + { "id": "208", "text": "208" }, + { "id": "210", "text": "210" }, + { "id": "237", "text": "237" }, + { "id": "238", "text": "238" }, + { "id": "239", "text": "239" }, + { "id": "243", "text": "243" }, + { "id": "245", "text": "245" }, + { "id": "248", "text": "248" }, + { "id": "249", "text": "249" }, + { "id": "258", "text": "258" }, + { "id": "266", "text": "266" }, + { "id": "267", "text": "267" }, + { "id": "273", "text": "273" }, + { "id": "277_inc_d", "text": "277 inc. d" }, + { "id": "281", "text": "281" }, + { "id": "289_inc1", "text": "289 inc. 1" }, + { "id": "289_inc3", "text": "289 inc. 3" }, + { "id": "292_parr_1", "text": "292 párr. 1" }, + { "id": "292_parr_2", "text": "292 párr. 2" }, + { "id": "295", "text": "295" }, + { "id": "296", "text": "296" }, + { "id": "301_bis", "text": "301 bis" } + ], + "CODIGO_O_LEY": [ + { "id": "codigo_contravencional", "text": "Código contravencional" }, + { "id": "codigo_penal_de_la_nacion", "text": "Código penal de la Nación" }, + { "id": "ley_1217", "text": "Ley 1217" }, + { "id": "ley_12331", "text": "Ley 12331" }, + { "id": "ley_13944", "text": "Ley 13944" }, + { "id": "ley_14346", "text": "Ley 14346" }, + { "id": "ley_23098", "text": "Ley 23098" }, + { "id": "ley_23592", "text": "Ley 23592" }, + { "id": "ley_23737", "text": "Ley 23737" }, + { "id": "ley_24192", "text": "Ley 24192" }, + { "id": "ley_24270", "text": "Ley 24270" }, + { "id": "ley_24769", "text": "Ley 24769" }, + { "id": "ley_25761", "text": "Ley 25761" }, + { "id": "ley_26735", "text": "Ley 26735" }, + { "id": "ley_451", "text": "Ley 451" } + ], + "NACIONALIDAD": [ + { "id": "argentina", "text": "Argentina" }, + { "id": "armenia", "text": "Armenia" }, + { "id": "australiana", "text": "Australiana" }, + { "id": "belga", "text": "Belga" }, + { "id": "boliviana", "text": "Boliviana" }, + { "id": "brasilera", "text": "Brasilera" }, + { "id": "chilena", "text": "Chilena" }, + { "id": "china", "text": "China" }, + { "id": "colombiana", "text": "Colombiana" }, + { "id": "cubana", "text": "Cubana" }, + { "id": "dominicana", "text": "Dominicana" }, + { "id": "ecuatoriana", "text": "Ecuatoriana" }, + { "id": "española", "text": "Española" }, + { "id": "estadounidense", "text": "Estadounidense" }, + { "id": "estonia", "text": "Estonia" }, + { "id": "francesa", "text": "Francesa" }, + { "id": "italiana", "text": "Italiana" }, + { "id": "kazaka", "text": "Kazaka" }, + { "id": "paraguaya", "text": "Paraguaya" }, + { "id": "peruana", "text": "Peruana" }, + { "id": "rusa", "text": "Rusa" }, + { "id": "senegalesa", "text": "Senegalesa" }, + { "id": "surcoreana", "text": "Surcoreana" }, + { "id": "ucraniana", "text": "Ucraniana" }, + { "id": "uruguaya", "text": "Uruguaya" }, + { "id": "venezolana", "text": "Venezolana" }, + { "id": "no_corresponde", "text": "No corresponde" }, + { "id": "sd", "text": "s/d" } + ], + "PERSONA_ACUSADA_NO_DETERMINADA": [ + { "id": "manifestantes", "text": "Manifestantes" }, + { "id": "organo_jurisdiccional", "text": "Órgano jurisdiccional" }, + { "id": "pagina_web", "text": "Página web" }, + { "id": "persona_juridica", "text": "Persona jurídica" }, + { "id": "personal_policial", "text": "Personal policial" }, + { "id": "usuario_de_chatstep", "text": "Usuario de Chatstep" }, + { + "id": "usuario_de_cuenta_de_google", + "text": "Usuario de cuenta de Google" + }, + { "id": "usuario_de_facebook", "text": "Usuario de Facebook" }, + { "id": "usuario_de_imgur", "text": "Usuario de Imgur" }, + { "id": "usuario_de_instagram", "text": "Usuario de Instagram" }, + { "id": "usuario_de_mercado_libre", "text": "Usuario de Mercado Libre" }, + { "id": "usuario_de_outlook", "text": "Usuario de Outlook" }, + { "id": "usuario_de_skout", "text": "Usuario de Skout" }, + { "id": "usuario_de_skype", "text": "Usuario de Skype" }, + { "id": "usuario_de_tiktok", "text": "Usuario de Tiktok" }, + { "id": "usuario_de_twitter", "text": "Usuario de Twitter" }, + { "id": "usuario_de_whatsapp", "text": "Usuario de WhatsApp" }, + { "id": "usuario_de_youtube", "text": "Usuario de Youtube" }, + { "id": "usuario_microsoft", "text": "Usuario Microsoft" }, + + { "id": "no_corresponde", "text": "No corresponde" }, + { "id": "sd", "text": "s/d" } + ], + "HIJOS_HIJAS_EN_COMUN": [ + { "id": "si", "text": "Si" }, + { "id": "no", "text": "No" } + ], + + "RELACION_Y_TIPO_ENTRE_ACUSADO_Y_DENUNCIANTE": [ + { "id": "familiar", "text": "Familiar" }, + { "id": "pareja", "text": "Pareja" }, + { "id": "ex_pareja", "text": "Ex pareja" }, + { "id": "jefe_empleada", "text": "Jefe empleada" }, + { "id": "familiar_de_ex_pareja", "text": "Familiar de ex pareja" }, + { "id": "pareja_de_la_madre", "text": "Pareja de la madre" }, + { "id": "ninguna", "text": "Ninguna" }, + { "id": "vecino", "text": "Vecino/a" }, + { "id": "profesor", "text": "Profesor" }, + { + "id": "propietario_del_inmueble_de_residencia", + "text": "Propietario del inmueble de residencia" + }, + { "id": "compañero_de_trabajo", "text": "Compañero/a de trabajo" }, + { "id": "amigo", "text": "Amigo/a" }, + { "id": "familiar_de_pareja", "text": "Familiar de pareja" }, + { "id": "s/d", "text": "S/D" }, + { "id": "no_corresponde", "text": "No corresponde" } + ], + "MEDIDAS_DE_PROTECCION_VIGENTES_AL_MOMENTO_DEL_HECHO": [ + { "id": "si", "text": "Si" }, + { "id": "no", "text": "No" } + ], + "MATERIA": [ + { "id": "allanamiento_autonomo", "text": "Allanamiento autónomo" }, + { "id": "habeas_corpus", "text": "Habeas corpus" }, + { "id": "ejecucion_de_multa", "text": "Ejecución de multa" }, + { "id": "amparo", "text": "Amparo" }, + { "id": "contravencional", "text": "Contravencional" }, + { "id": "faltas", "text": "Faltas" }, + { "id": "penal", "text": "Penal" } + ] +} diff --git a/frontend/src/renderer/src/components/validate-dataset/form-group/index.tsx b/frontend/src/renderer/src/components/validate-dataset/form-group/index.tsx new file mode 100644 index 00000000..e2009660 --- /dev/null +++ b/frontend/src/renderer/src/components/validate-dataset/form-group/index.tsx @@ -0,0 +1,97 @@ +import { useEffect, useState } from "react"; + +import { DecisionTabs } from "@/components"; +import { useFileDispatch, useForm } from "@/hooks"; +import { appendValidation } from "@/reducers/file/actions"; +import type { DocFile } from "@/types/file"; +import { Suggester, countDecisiones } from "@/utils/predictions"; +import Container from "./FormGroup.styles"; +import { + DatosAcusado, + DatosDenunciante, + Decision, + InfoGral, + InfoHecho, +} from "./forms"; + +interface Props { + file: DocFile; + onCheck: (checked: boolean) => void; +} +export default function FormGroup({ file, onCheck }: Props) { + const [decision, setDecision] = useState(0); + + const [checkedInfoGral, setCheckedInfoGral] = useState(false); + const [checkedInfoHecho, setCheckedInfoHecho] = useState(false); + const [checkedDecision, setCheckedDecision] = useState(false); + const [checkedDatosDenunciante, setCheckedDatosDenunciante] = useState(false); + const [checkedDatosAcusado, setCheckedDatosAcusado] = useState(false); + + const { register, submit, addDecision, decisionAmount, getDecisionValue } = + useForm(countDecisiones(file.predictions)); + const dispatch = useFileDispatch(); + + const createDecision = () => { + addDecision(); + }; + const selectDecision = (n: number) => setDecision(n); + + const handleSubmit = submit((data) => { + console.log({ data }); + dispatch(appendValidation(file.data.name, data)); + }); + + const props = { + register, + onSubmit: handleSubmit, + suggester: new Suggester(file.predictions), + }; + const decisionProps = { + ...props, + getDecisionValue, + }; + + useEffect(() => { + onCheck( + checkedInfoGral && + checkedInfoHecho && + checkedDecision && + checkedDatosDenunciante && + checkedDatosAcusado, + ); + }, [ + onCheck, + checkedInfoGral, + checkedInfoHecho, + checkedDecision, + checkedDatosDenunciante, + checkedDatosAcusado, + ]); + + return ( + + + + + + + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/validate-dataset/index.tsx b/frontend/src/renderer/src/components/validate-dataset/index.tsx new file mode 100644 index 00000000..c5b0f124 --- /dev/null +++ b/frontend/src/renderer/src/components/validate-dataset/index.tsx @@ -0,0 +1,126 @@ +import { useState } from "react"; + +import { Button, FileAnnotator, FileStepper, Grid } from "@/components"; +import Footer from "@/components/layout/footer"; +import { useFileDispatch, useFiles } from "@/hooks"; +import { SectionTitle } from "@/layout/section-title"; +import { validate } from "@/reducers/file/actions"; +import { css } from "@/styled/css"; +import { HStack, Stack } from "@/styled/jsx"; +import { isFileValidated, isValidationCompleted } from "@/utils/file"; +import { getFeatureRouteSlug, parseFeatureRouteSlug } from "@/types/features"; +import { Navigate, useNavigate, useParams } from "@tanstack/react-router"; +import FormGroup from "./form-group"; +import { moveNext, movePrevious } from "./utils"; + +export function ValidateDataset() { + // HOOKS + const { feature: featureSlug } = useParams({ from: "/$feature/validation" }); + const feature = parseFeatureRouteSlug(featureSlug); + + if (!feature) return ; + const files = useFiles(); + const [checked, setChecked] = useState(false); + const [selected, setSelected] = useState(0); + const dispatch = useFileDispatch(); + const navigate = useNavigate(); + + // FIELDS + const hasStepper = files.length > 1; + + const selectedFile = files[selected]; + // Check if the validation was completed on all the files + const canContinue = isValidationCompleted(files); + const canValidate = isFileValidated(selectedFile); + + // HANDLERS + const moveIndex = (newIndex: number | undefined) => { + if (newIndex !== undefined) setSelected(newIndex); + }; + const nextFile = () => moveIndex(moveNext(selected, files)); + const previousFile = () => moveIndex(movePrevious(selected, files)); + + const handleContinue = () => { + navigate({ + to: "/$feature/finish", + params: { feature: getFeatureRouteSlug(feature) }, + }); + }; + + const handleValidate = async () => { + // Set validated = true so the file is no longer accesible through the FileStepper component + dispatch(validate(selectedFile.data.name)); + + if (canContinue || !hasStepper) { + handleContinue(); + } else { + nextFile(); + } + }; + + const handleCheck = (checked: boolean) => { + setChecked(checked); + }; + + return ( + + + + + + 3. Validación de datos + + + + +
+ + {hasStepper && ( + + )} + + {canContinue ? ( + + ) : ( + + )} + +
+
+ ); +} diff --git a/frontend/src/renderer/src/components/validate-dataset/utils.ts b/frontend/src/renderer/src/components/validate-dataset/utils.ts new file mode 100644 index 00000000..6cc7ca6d --- /dev/null +++ b/frontend/src/renderer/src/components/validate-dataset/utils.ts @@ -0,0 +1,37 @@ +import type { DocFile } from "@/types/file"; + +/** + * Finds the next available index and returns it + * @param cur Current `selected` index + * @param state State array of files + * @returns The next available index + */ +export function movePrevious(cur: number, state: DocFile[]) { + let index = cur; + + do { + index--; + + if (index < 0) return undefined; + } while (state[index].validated); + + return index; +} + +/** + * Finds the previous available index and returns it + * @param cur Current `selected` index + * @param state State array of files + * @returns The previous available index + */ +export function moveNext(cur: number, state: DocFile[]) { + let index = cur; + + do { + index++; + + if (index === state.length) return undefined; + } while (state[index].validated); + + return index; +} diff --git a/frontend/src/renderer/src/components/validation-form/ValidationForm.styles.ts b/frontend/src/renderer/src/components/validation-form/ValidationForm.styles.ts new file mode 100644 index 00000000..36adf124 --- /dev/null +++ b/frontend/src/renderer/src/components/validation-form/ValidationForm.styles.ts @@ -0,0 +1,7 @@ +import { styled } from "@/styles"; + +export const Form = styled("form", { + display: "flex", + flexDirection: "column", + gap: "$l", +}); diff --git a/frontend/src/renderer/src/components/validation-form/index.tsx b/frontend/src/renderer/src/components/validation-form/index.tsx new file mode 100644 index 00000000..3d15eecf --- /dev/null +++ b/frontend/src/renderer/src/components/validation-form/index.tsx @@ -0,0 +1,76 @@ +import { CheckCircle } from "phosphor-react"; +import { + Children, + type FormEvent, + type FormEventHandler, + type ReactElement, + type ReactNode, + cloneElement, + isValidElement, + useEffect, + useState, +} from "react"; + +import { Button } from "@/components"; +import type { NativeComponent } from "@/types/component"; + +import { css } from "@/styled/css"; +import { styled } from "@/styled/jsx"; +import { Form } from "./ValidationForm.styles"; + +interface Props extends NativeComponent<"form"> { + children: ReactNode; + title: string; + onSubmit: FormEventHandler; + onCheck: (checked: boolean) => void; +} +export default function ValidationForm({ + children, + title, + onSubmit, + onCheck, + ...props +}: Props) { + const [checked, setChecked] = useState(false); + + const handleClick = () => setChecked(true); + + const onChange = () => { + if (checked) setChecked(false); + }; + + // Add the onChange handler to every children + const childrenWithHandler = Children.map(children, (child) => { + if (isValidElement(child)) { + return cloneElement(child as ReactElement<{ onChange: () => void }>, { onChange }); + } + return child; + }); + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + + onSubmit(e); + }; + + useEffect(() => { + onCheck(checked); + }, [checked, onCheck]); + + return ( +
+ {title} + {childrenWithHandler} + +
+ ); +} diff --git a/frontend/src/renderer/src/constants/anonymizer-categories.ts b/frontend/src/renderer/src/constants/anonymizer-categories.ts new file mode 100644 index 00000000..1300ddc0 --- /dev/null +++ b/frontend/src/renderer/src/constants/anonymizer-categories.ts @@ -0,0 +1,82 @@ +import { type AnonymizerLabels, anonymizerLabels } from "@/types/aymurai"; + +export const FALLBACK_ANONYMIZER_CATEGORY = "Otros datos sensibles"; + +export const ANONYMIZER_CATEGORY_LABEL_IDS = { + Roles: [ + "PER", + "DENUNCIANTE", + "ACUSADO/A", + "TESTIGO/A", + "NIÑO/A_ADOSLECENTE", + ], + Lugares: ["LOC", "DIRECCION"], + "Datos personales": ["DNI", "EDAD", "NACIONALIDAD", "ESTUDIOS"], + "Contacto e identificadores digitales": [ + "TELEFONO", + "CORREO_ELECTRONICO", + "USUARIX", + "IP", + "LINK", + "NOMBRE_ARCHIVO", + ], + "Datos judiciales": ["NUM_EXPEDIENTE", "NUM_ACTUACION", "CAUSA", "CUIJ"], + "Datos administrativos y registrales": [ + "CUIT_CUIL", + "AFILIADO", + "NUM_MATRICULA", + "PATENTE_DOMINIO", + ], + "Datos bancarios": ["BANCO", "CBU", "NUM_CAJA_AHORRO"], + Fechas: ["FECHA"], + [FALLBACK_ANONYMIZER_CATEGORY]: [ + "MARCA_AUTOMOVIL", + "INSTITUCION", + "TEXTO_ANONIMIZAR", + ], +} as const satisfies Record; + +export const ANONYMIZER_CATEGORY_NAMES = Object.keys( + ANONYMIZER_CATEGORY_LABEL_IDS, +); + +const labelById = new Map(anonymizerLabels.map((label) => [label.id, label])); + +export const ANONYMIZER_LABEL_CATEGORY = Object.fromEntries( + Object.entries(ANONYMIZER_CATEGORY_LABEL_IDS).flatMap( + ([category, labelIds]) => labelIds.map((labelId) => [labelId, category]), + ), +) as Partial>; + +export function getAnonymizerCategoryForLabel(labelId: string): string { + return ( + ANONYMIZER_LABEL_CATEGORY[labelId as AnonymizerLabels] ?? + FALLBACK_ANONYMIZER_CATEGORY + ); +} + +export function getAnonymizerLabelsForCategory(category: string) { + const configuredIds = ANONYMIZER_CATEGORY_LABEL_IDS[category] ?? []; + const configuredLabels = configuredIds + .map((labelId) => labelById.get(labelId)) + .filter((label) => label !== undefined); + + if (category !== FALLBACK_ANONYMIZER_CATEGORY) return configuredLabels; + + const configuredIdSet = new Set( + Object.values(ANONYMIZER_CATEGORY_LABEL_IDS).flat(), + ); + const uncategorizedLabels = anonymizerLabels.filter( + (label) => !configuredIdSet.has(label.id), + ); + + return [...configuredLabels, ...uncategorizedLabels]; +} + +export function getLabelsPrioritizingCategory(category: string) { + const categoryLabels = getAnonymizerLabelsForCategory(category); + const categoryIds = new Set(categoryLabels.map((label) => label.id)); + const rest = anonymizerLabels.filter((label) => !categoryIds.has(label.id)); + + return [...categoryLabels, ...rest]; +} diff --git a/frontend/src/renderer/src/constants/config.ts b/frontend/src/renderer/src/constants/config.ts new file mode 100644 index 00000000..f8968453 --- /dev/null +++ b/frontend/src/renderer/src/constants/config.ts @@ -0,0 +1,6 @@ +export const DATAGENERO_URL = "https://www.datagenero.org/"; + +/** + * Only allow these extensions to be analyzed + */ +export const WHITELISTED_EXTENSIONS = ["docx", "pdf"]; diff --git a/frontend/src/renderer/src/constants/excluded-tags.ts b/frontend/src/renderer/src/constants/excluded-tags.ts new file mode 100644 index 00000000..3d423f8c --- /dev/null +++ b/frontend/src/renderer/src/constants/excluded-tags.ts @@ -0,0 +1,8 @@ +import { type AnonymizerLabels, anonymizerLabels } from "@/types/aymurai"; + +const entries = anonymizerLabels.map((label) => [label.id, true] as const); + +export const EXCLUDED_TAGS = Object.fromEntries(entries) as Record< + AnonymizerLabels, + boolean +>; diff --git a/frontend/src/renderer/src/constants/i18n/index.ts b/frontend/src/renderer/src/constants/i18n/index.ts new file mode 100644 index 00000000..06b788ca --- /dev/null +++ b/frontend/src/renderer/src/constants/i18n/index.ts @@ -0,0 +1,27 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import es from "./locales/es"; + +// --- TypeScript type augmentation --- +// Tells i18next the exact shape of every namespace, +// so t('key') calls are fully typed and autocompleted. +declare module "i18next" { + interface CustomTypeOptions { + defaultNS: "common"; + resources: typeof es; + } +} + +i18n.use(initReactI18next).init({ + lng: "es", + fallbackLng: "es", + defaultNS: "common", + resources: { + es, + }, + interpolation: { + escapeValue: false, // React already escapes values + }, +}); + +export default i18n; diff --git a/frontend/src/renderer/src/constants/i18n/locales/es/anonymizer.ts b/frontend/src/renderer/src/constants/i18n/locales/es/anonymizer.ts new file mode 100644 index 00000000..5fbaada6 --- /dev/null +++ b/frontend/src/renderer/src/constants/i18n/locales/es/anonymizer.ts @@ -0,0 +1,73 @@ +import { WHITELISTED_EXTENSIONS } from "@/constants/config"; + +const validExtensions = WHITELISTED_EXTENSIONS.map((e) => `.${e}`).join(", "); + +const anonymizer = { + title: "Anonimizador", + subtitle: "Anonimiza resoluciones judiciales de manera automática y editable", + onboarding: { + sectionTitle: "1. Selección de Archivo", + validFormats: `Formatos válidos: ${validExtensions}`, + loadDocuments: "Cargar documentos", + dropAreaTitle: "Selecciona el archivo para anonimizar", + dropAreaFormats: `Formatos válidos: ${validExtensions}`, + }, + preview: { + sectionTitle: "1. Selección de Archivo", + filesLabel: "Archivos seleccionados", + validFormats: `Formatos válidos: ${validExtensions}`, + loadMore: "Cargar más documentos", + continue: "Continuar", + }, + howItWorks: { + step1: { + alt: "Interfaz web con selector y cursor", + title: "Selecciona la resolución judicial", + subtitle: "Sube el documento que quieres anonimizar.", + }, + step2: { + alt: "Barra de búsqueda con cursor", + title: "La inteligencia artificial analiza el documento", + subtitle: + "Reconoce automáticamente la información a anonimizar.", + }, + step3: { + alt: "Visor de documentos con controles de revisión", + title: "Revisión y validación humana", + subtitle: + "Es importante que verifiques que los datos sean correctos antes de exportar el archivo.", + }, + step4: { + alt: "Binoculares con globo terráqueo", + title: "Generación del documento anonimizado", + subtitle: + "Proceso terminado. El documento está listo para ser exportado.", + }, + }, + process: { + sectionTitle: "2. Procesamiento del archivo", + processingTitle: "AymurAI está extrayendo los datos del archivo", + processingSubtitle: "Este proceso puede tardar algunos minutos.", + finishText: "Se finalizó el análisis del documento.", + }, + validation: { + toastSingleSuccess: + 'Se aplicó con éxito una etiqueta de "{{label}}" en una ocurrencia.', + toastAllSuccess: + 'Se aplicó con éxito una etiqueta de "{{label}}" en todas las ocurrencias.', + }, + result: { sectionTitle: "" }, + finish: { + sectionTitle: "4. Finalización", + description: + "Los datos encontrados por AymurAI y posteriormente validados ya han sido anonimizados correctamente.", + subtitle: "Archivo procesado", + restart: "Cargar un nuevo documento", + viewResult: "Descargar ODT", + viewResultPDF: "Descargar PDF", + downloadError: + "Ocurrió un error al generar el archivo. Por favor, intente nuevamente.", + }, +}; + +export default anonymizer; diff --git a/frontend/src/renderer/src/constants/i18n/locales/es/common.ts b/frontend/src/renderer/src/constants/i18n/locales/es/common.ts new file mode 100644 index 00000000..9d4bfac6 --- /dev/null +++ b/frontend/src/renderer/src/constants/i18n/locales/es/common.ts @@ -0,0 +1,39 @@ +const common = { + back: "Volver", + howItWorks: "¿Cómo funciona?", + platformBuiltBy: "Plataforma hecha por", + settings: "Configuración", + home: { + features: { + greeting: "¡Hola! Selecciona la herramienta a utilizar", + }, + host: { + howToConnect: "¿Cómo deseas conectarte a Aymurai?", + optionLocal: "Local", + optionServer: "Servidor", + optionOr: "o", + connectServerExplanation: + "Ingresa la dirección del servidor al que deseas conectarte", + connectServerLabel: "Dirección del servidor", + connectServerSubmit: "Guardar y conectar", + errors: { + network: "No se pudo conectar al servidor", + connection: "Error de conexión", + invalidResponse: "El servidor no respondió correctamente", + invalidUrl: "El formato de la URL es incorrecto.", + unknown: "Error desconocido", + }, + }, + }, + stepper: { + selection: "Selección", + extraction: "Extracción", + validation: "Validación", + finalization: "Finalización", + }, + filePreview: { + loadError: "No se pudo cargar el archivo", + }, +}; + +export default common; diff --git a/frontend/src/renderer/src/constants/i18n/locales/es/dataset.ts b/frontend/src/renderer/src/constants/i18n/locales/es/dataset.ts new file mode 100644 index 00000000..de7fa05f --- /dev/null +++ b/frontend/src/renderer/src/constants/i18n/locales/es/dataset.ts @@ -0,0 +1,64 @@ +import { WHITELISTED_EXTENSIONS } from "@/constants/config"; + +const validExtensions = WHITELISTED_EXTENSIONS.map((e) => `.${e}`).join(", "); + +const dataset = { + title: "Set de datos", + subtitle: "Convierte resoluciones judiciales en set de datos estructurados", + onboarding: { + sectionTitle: "1. Selección de Archivos", + validFormats: `Formatos válidos: ${validExtensions}`, + loadDocuments: "Cargar documentos", + dropAreaTitle: "Selecciona el archivo para\nagregar a la base de datos", + dropAreaFormats: `Formatos válidos: ${validExtensions}`, + }, + preview: { + sectionTitle: "1. Selección de Archivos", + filesLabel: "Archivos seleccionados", + validFormats: `Formatos válidos: ${validExtensions}`, + loadMore: "Cargar más documentos", + continue: "Continuar", + }, + howItWorks: { + step1: { + alt: "Interfaz web con selector y cursor", + title: "Selecciona las resoluciones judiciales", + subtitle: "Sube los documentos que quieres incorporar al set de datos.", + }, + step2: { + alt: "Barra de búsqueda con cursor", + title: "La inteligencia artificial analiza los documentos", + subtitle: + "Extrae automáticamente la información relevante de cada documento.", + }, + step3: { + alt: "Visor de documentos con controles de revisión", + title: "Revisión y validación humana", + subtitle: + "Es importante que verifiques que los datos sean correctos antes de exportar el archivo.", + }, + step4: { + alt: "Binoculares con globo terráqueo", + title: "Generación del set de datos", + subtitle: + "Los documentos pasan a formar parte del set de datos abiertos.", + }, + }, + process: { + sectionTitle: "2. Extracción de datos", + processingTitle: "AymurAI está extrayendo los datos de los archivos", + processingSubtitle: "Este proceso puede tardar algunos minutos.", + finishText: "Se finalizó el análisis de tus documentos.", + }, + result: { sectionTitle: "" }, + finish: { + sectionTitle: "4. Finalización", + description: + "Los datos encontrados por AymurAI y posteriormente validados ya son parte del set de datos abiertos con perspectiva de género.", + subtitle: "Archivos procesados", + restart: "Cargar más documentos", + viewResult: "Ver set de datos", + }, +}; + +export default dataset; diff --git a/frontend/src/renderer/src/constants/i18n/locales/es/index.ts b/frontend/src/renderer/src/constants/i18n/locales/es/index.ts new file mode 100644 index 00000000..d3cf487c --- /dev/null +++ b/frontend/src/renderer/src/constants/i18n/locales/es/index.ts @@ -0,0 +1,11 @@ +import anonymizer from "./anonymizer"; +import common from "./common"; +import dataset from "./dataset"; + +const es = { + common, + dataset, + anonymizer, +}; + +export default es; diff --git a/frontend/src/renderer/src/context/Annotation/index.tsx b/frontend/src/renderer/src/context/Annotation/index.tsx new file mode 100644 index 00000000..4404e057 --- /dev/null +++ b/frontend/src/renderer/src/context/Annotation/index.tsx @@ -0,0 +1,792 @@ +import type { + LabelAnnotation, + SearchAnnotation, +} from "@/components/file-annotator/types"; +import ManualEntityResolutionDialog, { + type ManualEntityResolutionRequest, +} from "@/components/file/manual-entity-resolution-dialog"; +import { getAnonymizerCategoryForLabel } from "@/constants/anonymizer-categories"; +import { EXCLUDED_TAGS } from "@/constants/excluded-tags"; +import { showToast } from "@/features/showToast"; +import { useEntityGroups, useFileDispatch, useFiles } from "@/hooks"; +import { + appendPrediction, + removePrediction, + removePredictionsByText, + updatePredictionLabel, + updatePredictionsByCanonicalId, + updatePredictionsByText, +} from "@/reducers/file/actions"; +import { useHoverState } from "@/store/useHoverState"; +import { + useExcludedTagsConfig, + useGroupOrder, + useGroupOrderActions, +} from "@/store/useLocal"; +import type { + AllLabels, + AllLabelsWithSufix, + AnonymizerLabels, + PredictLabel, +} from "@/types/aymurai"; +import { anonymizerLabels } from "@/types/aymurai"; +import type { DocFile, Paragraph } from "@/types/file"; +import { + type ManualEntityResolution, + findSimilarEntityGroups, + normalizeEntityText, + stripEntityLabelSuffix, +} from "@/utils/anonymizer/entity-similarity"; +import { findNormalizedOccurrenceRanges } from "@/utils/anonymizer/occurrences"; +import { filterActivePredictions } from "@/utils/anonymizer/predictions"; +import { Check, WarningCircle } from "phosphor-react"; +import { + type ReactNode, + createContext, + useCallback, + useContext, + useMemo, + useState, +} from "react"; +import { findSearchIndexes, getSelectionAnnotationRange } from "./utils"; + +interface AnnotationContextValues { + isAnnotable: boolean; + label: AllLabels | null; + add: (prediction: PredictLabel) => void; + remove: (prediction: PredictLabel) => void; + removeByText: (prediction: PredictLabel) => void; + updateLabel: ( + prediction: PredictLabel, + newLabel: AllLabels | AllLabelsWithSufix, + ) => void; + updateByText: ( + prediction: PredictLabel, + newLabel: AllLabels | AllLabelsWithSufix, + ) => void; + updateByCanonicalId: ( + canonicalId: string, + newLabel: AllLabels | AllLabelsWithSufix, + ) => void; + addBySearch: (search: string, label: AllLabels) => void; +} + +/** + * Provides the token received through OAuth2 login to the whole app + */ +export const AnnotationContext = createContext({ + isAnnotable: false, + label: null, + add: () => {}, + remove: () => {}, + removeByText: () => {}, + updateLabel: () => {}, + updateByText: () => {}, + updateByCanonicalId: () => {}, + addBySearch: () => {}, +}); +AnnotationContext.displayName = "AnnotationContext"; + +interface Props { + children?: ReactNode; + file: DocFile; + isAnnotable?: boolean; + label: AllLabels | null; +} + +type PendingManualResolution = ManualEntityResolutionRequest & { + predictions: PredictLabel[]; + selectedLabel: AllLabels; +}; + +const ANONYMIZER_LABEL_IDS = new Set( + anonymizerLabels.map((item) => item.id), +); + +function toBaseLabel(label: AllLabels | AllLabelsWithSufix): AllLabels { + return stripEntityLabelSuffix(String(label)) as AllLabels; +} + +function isKnownAnonymizerLabel(label: string): label is AnonymizerLabels { + return ANONYMIZER_LABEL_IDS.has(label); +} + +function isLabelEnabled( + label: AllLabels, + excludedTags: Record | null, +): boolean { + const baseLabel = stripEntityLabelSuffix(String(label)); + if (!isKnownAnonymizerLabel(baseLabel)) return true; + return (excludedTags ?? EXCLUDED_TAGS)[baseLabel] !== false; +} + +function isExcludedMention(text: string, excludedWords: string[]): boolean { + const normalizedText = normalizeEntityText(text); + return excludedWords.some( + (word) => normalizeEntityText(word) === normalizedText, + ); +} + +function withCanonicalEntity( + prediction: PredictLabel, + canonicalId: string, + label: AllLabels | AllLabelsWithSufix, +): PredictLabel { + return { + ...prediction, + attrs: { + ...prediction.attrs, + aymurai_label: label, + canonical_entity_id: canonicalId, + }, + }; +} + +function compareAppearance( + a: { paragraphIndex: number; start_char: number }, + b: { paragraphIndex: number; start_char: number }, +) { + if (a.paragraphIndex !== b.paragraphIndex) { + return a.paragraphIndex - b.paragraphIndex; + } + return a.start_char - b.start_char; +} + +function rangesOverlap( + a: { start_char: number; end_char: number }, + b: { start_char: number; end_char: number }, +) { + return a.start_char < b.end_char && b.start_char < a.end_char; +} + +function hasInternalOverlap(predictions: PredictLabel[]) { + const byParagraph = new Map(); + for (const prediction of predictions) { + const paragraphPredictions = byParagraph.get(prediction.paragraphId) ?? []; + paragraphPredictions.push(prediction); + byParagraph.set(prediction.paragraphId, paragraphPredictions); + } + + for (const paragraphPredictions of byParagraph.values()) { + const sorted = [...paragraphPredictions].sort( + (a, b) => a.start_char - b.start_char, + ); + for (let i = 1; i < sorted.length; i += 1) { + const previous = sorted[i - 1]; + const current = sorted[i]; + if (previous && current && rangesOverlap(previous, current)) return true; + } + } + + return false; +} + +export default function AnnotationProvider({ + children, + file, + isAnnotable = false, + label, +}: Props) { + const dispatch = useFileDispatch(); + const files = useFiles(); + const { tags, words } = useExcludedTagsConfig(); + const { groupOrder } = useGroupOrder(); + const { setGroupOrder } = useGroupOrderActions(); + const setLastEditedCanonicalId = useHoverState( + (s) => s.setLastEditedCanonicalId, + ); + const [pendingResolution, setPendingResolution] = + useState(null); + + const activeFiles = useMemo( + () => + files.map((item) => ({ + ...item, + predictions: filterActivePredictions(item.predictions, tags, words), + })), + [files, tags, words], + ); + const groups = useEntityGroups(activeFiles); + const activePredictionsByParagraph = useMemo(() => { + const result = new Map(); + const activePredictions = filterActivePredictions( + file.predictions, + tags, + words, + ); + + for (const prediction of activePredictions) { + const paragraphPredictions = result.get(prediction.paragraphId) ?? []; + paragraphPredictions.push(prediction); + result.set(prediction.paragraphId, paragraphPredictions); + } + + return result; + }, [file.predictions, tags, words]); + + const paragraphIndexById = useMemo(() => { + const result = new Map(); + for (const currentFile of files) { + for (const [index, paragraph] of ( + currentFile.paragraphs ?? [] + ).entries()) { + if (!result.has(paragraph.id)) result.set(paragraph.id, index); + } + } + return result; + }, [files]); + + const getFirstAppearance = useCallback( + (predictions: PredictLabel[]) => { + return predictions.reduce( + (earliest, prediction) => { + const current = { + paragraphIndex: paragraphIndexById.get(prediction.paragraphId) ?? 0, + start_char: prediction.start_char, + }; + return compareAppearance(current, earliest) < 0 ? current : earliest; + }, + { + paragraphIndex: Number.POSITIVE_INFINITY, + start_char: Number.POSITIVE_INFINITY, + }, + ); + }, + [paragraphIndexById], + ); + + const updateGroupOrderForNewGroup = useCallback( + ( + canonicalId: string, + selectedLabel: AllLabels, + predictions: PredictLabel[], + ) => { + const category = getAnonymizerCategoryForLabel(String(selectedLabel)); + const existingOrder = groupOrder?.[category]; + if (!existingOrder) return; + + const firstAppearance = getFirstAppearance(predictions); + const categoryGroups = groups + .filter( + (group) => + getAnonymizerCategoryForLabel(group.renderBase) === category, + ) + .sort((a, b) => + compareAppearance(a.firstAppearance, b.firstAppearance), + ); + const nextOrder = existingOrder.filter((id) => id !== canonicalId); + const insertBefore = categoryGroups.find( + (group) => + compareAppearance(firstAppearance, group.firstAppearance) < 0, + ); + + if (!insertBefore) { + nextOrder.push(canonicalId); + } else { + const index = nextOrder.indexOf(insertBefore.canonicalId); + if (index === -1) nextOrder.push(canonicalId); + else nextOrder.splice(index, 0, canonicalId); + } + + setGroupOrder({ ...groupOrder, [category]: nextOrder }); + }, + [getFirstAppearance, groupOrder, groups, setGroupOrder], + ); + + const commitPredictions = useCallback( + ( + predictions: PredictLabel[], + canonicalId: string, + resolvedLabel: AllLabels | AllLabelsWithSufix, + ) => { + for (const prediction of predictions) { + dispatch( + appendPrediction( + file.data.name, + withCanonicalEntity(prediction, canonicalId, resolvedLabel), + ), + ); + } + setLastEditedCanonicalId(canonicalId); + showToast( + predictions.length > 1 + ? "Se agregaron las etiquetas al grupo de entidad." + : "Se agregó la etiqueta al grupo de entidad.", + "success", + Check, + ); + }, + [dispatch, file.data.name, setLastEditedCanonicalId], + ); + + const commitNewGroup = useCallback( + (predictions: PredictLabel[], selectedLabel: AllLabels) => { + const canonicalId = crypto.randomUUID(); + commitPredictions(predictions, canonicalId, selectedLabel); + updateGroupOrderForNewGroup(canonicalId, selectedLabel, predictions); + }, + [commitPredictions, updateGroupOrderForNewGroup], + ); + + const queueManualPredictions = useCallback( + ( + predictions: PredictLabel[], + selectedLabel: AllLabels | AllLabelsWithSufix, + ) => { + if (predictions.length === 0) return; + + const baseLabel = toBaseLabel(selectedLabel); + const text = predictions[0]?.text ?? ""; + if (!normalizeEntityText(text)) return; + + if (hasInternalOverlap(predictions)) { + showToast( + "No se pueden agregar ocurrencias solapadas entre sí.", + "error", + WarningCircle, + ); + return; + } + + const overlappingPrediction = predictions.find((prediction) => + (activePredictionsByParagraph.get(prediction.paragraphId) ?? []).some( + (activePrediction) => rangesOverlap(prediction, activePrediction), + ), + ); + if (overlappingPrediction) { + showToast( + "La selección se solapa con una entidad activa. Eliminá o editá la entidad existente antes de crear otra.", + "error", + WarningCircle, + ); + return; + } + + if (!isLabelEnabled(baseLabel, tags)) { + showToast( + "La categoría seleccionada está desactivada en Configuración.", + "error", + WarningCircle, + ); + return; + } + + if (isExcludedMention(text, words)) { + showToast( + "El término seleccionado está en la lista de términos excluidos.", + "error", + WarningCircle, + ); + return; + } + + const candidates = findSimilarEntityGroups( + { + text, + normalizedText: normalizeEntityText(text), + label: String(baseLabel), + paragraphId: predictions[0]?.paragraphId, + start_char: predictions[0]?.start_char, + end_char: predictions[0]?.end_char, + }, + groups, + ); + + if (candidates.length === 0) { + commitNewGroup(predictions, baseLabel); + return; + } + + setPendingResolution({ + text, + label: String(baseLabel), + count: predictions.length, + candidates, + predictions, + selectedLabel: baseLabel, + }); + }, + [activePredictionsByParagraph, commitNewGroup, groups, tags, words], + ); + + const add = useCallback( + (prediction: PredictLabel) => { + queueManualPredictions([prediction], prediction.attrs.aymurai_label); + }, + [queueManualPredictions], + ); + + const remove = useCallback( + (prediction: PredictLabel) => { + dispatch(removePrediction(file.data.name, prediction)); + }, + [dispatch, file.data.name], + ); + + const removeByText = useCallback( + (prediction: PredictLabel) => { + dispatch(removePredictionsByText(file.data.name, prediction.text)); + }, + [dispatch, file.data.name], + ); + + const updateLabel = useCallback( + (prediction: PredictLabel, newLabel: AllLabels | AllLabelsWithSufix) => { + dispatch(updatePredictionLabel(file.data.name, prediction, newLabel)); + }, + [dispatch, file.data.name], + ); + + const updateByText = useCallback( + (prediction: PredictLabel, newLabel: AllLabels | AllLabelsWithSufix) => { + const normalizedText = normalizeEntityText(prediction.text); + if (!normalizedText) return; + + const sourceBaseLabel = toBaseLabel(prediction.attrs.aymurai_label); + const nextBaseLabel = toBaseLabel(newLabel); + const sourceCanonicalId = prediction.attrs.canonical_entity_id ?? null; + const targetCanonicalId = + sourceCanonicalId && sourceBaseLabel === nextBaseLabel + ? sourceCanonicalId + : crypto.randomUUID(); + const missingPredictions: PredictLabel[] = []; + let skippedOverlaps = 0; + + file.paragraphs?.forEach((paragraph: Paragraph) => { + const ranges = findNormalizedOccurrenceRanges( + paragraph.value, + prediction.text, + ); + + ranges.forEach((range) => { + const draftRange = { + start_char: range.start, + end_char: range.end, + }; + const activeOverlaps = ( + activePredictionsByParagraph.get(paragraph.id) ?? [] + ).filter((activePrediction) => + rangesOverlap(draftRange, activePrediction), + ); + const hasEquivalentActiveOverlap = activeOverlaps.some( + (activePrediction) => + normalizeEntityText(activePrediction.text) === normalizedText, + ); + + if (hasEquivalentActiveOverlap) return; + + if (activeOverlaps.length > 0) { + skippedOverlaps += 1; + return; + } + + missingPredictions.push({ + mentionId: crypto.randomUUID(), + start_char: range.start, + end_char: range.end, + paragraphId: paragraph.id, + text: range.text, + attrs: { + aymurai_label: newLabel, + aymurai_label_subclass: null, + aymurai_alt_text: null, + aymurai_alt_start_char: range.start, + aymurai_alt_end_char: range.end, + }, + }); + }); + }); + + dispatch( + updatePredictionsByText( + file.data.name, + prediction.text, + newLabel, + targetCanonicalId, + ), + ); + + for (const missingPrediction of missingPredictions) { + dispatch( + appendPrediction( + file.data.name, + withCanonicalEntity(missingPrediction, targetCanonicalId, newLabel), + ), + ); + } + + const createdNewGroup = + !sourceCanonicalId || sourceBaseLabel !== nextBaseLabel; + if (createdNewGroup) { + updateGroupOrderForNewGroup( + targetCanonicalId, + nextBaseLabel, + missingPredictions.length > 0 ? missingPredictions : [prediction], + ); + } + + setLastEditedCanonicalId(targetCanonicalId); + + if (skippedOverlaps > 0) { + showToast( + "Algunas ocurrencias no se agregaron porque se solapan con entidades activas.", + "error", + WarningCircle, + ); + } + }, + [ + activePredictionsByParagraph, + dispatch, + file.data.name, + file.paragraphs, + setLastEditedCanonicalId, + updateGroupOrderForNewGroup, + ], + ); + + const updateByCanonicalId = useCallback( + (canonicalId: string, newLabel: AllLabels | AllLabelsWithSufix) => { + dispatch(updatePredictionsByCanonicalId(canonicalId, newLabel)); + }, + [dispatch], + ); + + const addBySearch = useCallback( + (search: string, label: AllLabels) => { + if (!search || search.length < 3) return; + + const normalizedSearch = normalizeEntityText(search); + const predictions: PredictLabel[] = []; + let skippedOverlaps = 0; + const reusableCanonicalId = [...activePredictionsByParagraph.values()] + .flat() + .find( + (prediction) => + normalizeEntityText(prediction.text) === normalizedSearch && + prediction.attrs.canonical_entity_id, + )?.attrs.canonical_entity_id; + + file.paragraphs?.forEach((paragraph: Paragraph) => { + const normalizedRanges = findNormalizedOccurrenceRanges( + paragraph.value, + search, + ); + const ranges = + normalizedRanges.length > 0 + ? normalizedRanges + : findSearchIndexes(paragraph.value, search).map((start) => ({ + start, + end: start + search.length, + text: paragraph.value.slice(start, start + search.length), + normalizedText: normalizeEntityText(search), + })); + ranges.forEach((range) => { + const draftRange = { + start_char: range.start, + end_char: range.end, + }; + const activeOverlaps = ( + activePredictionsByParagraph.get(paragraph.id) ?? [] + ).filter((activePrediction) => + rangesOverlap(draftRange, activePrediction), + ); + const hasEquivalentActiveOverlap = activeOverlaps.some( + (activePrediction) => + normalizeEntityText(activePrediction.text) === normalizedSearch, + ); + + if (hasEquivalentActiveOverlap) return; + + if (activeOverlaps.length > 0) { + skippedOverlaps += 1; + return; + } + + const prediction: PredictLabel = { + mentionId: crypto.randomUUID(), + start_char: range.start, + end_char: range.end, + paragraphId: paragraph.id, + text: range.text, + attrs: { + aymurai_label: label, + aymurai_label_subclass: null, + aymurai_alt_text: null, + aymurai_alt_start_char: range.start, + aymurai_alt_end_char: range.end, + }, + }; + predictions.push(prediction); + }); + }); + + if (predictions.length === 0 && skippedOverlaps > 0) { + showToast( + "No se agregaron ocurrencias porque todas se solapan con entidades activas.", + "error", + WarningCircle, + ); + return; + } + + if (reusableCanonicalId) { + dispatch( + updatePredictionsByText( + file.data.name, + search, + label, + reusableCanonicalId, + ), + ); + + for (const prediction of predictions) { + dispatch( + appendPrediction( + file.data.name, + withCanonicalEntity(prediction, reusableCanonicalId, label), + ), + ); + } + + setLastEditedCanonicalId(reusableCanonicalId); + if (skippedOverlaps > 0) { + showToast( + "Algunas ocurrencias no se agregaron porque se solapan con entidades activas.", + "error", + WarningCircle, + ); + } + return; + } + + queueManualPredictions(predictions, label); + }, + [ + activePredictionsByParagraph, + dispatch, + file.data.name, + file.paragraphs, + queueManualPredictions, + setLastEditedCanonicalId, + ], + ); + + const selectHandler = () => { + // If the user hasn't selected any tag to search, do nothing + if (!label) return; + + const selection = window.getSelection(); + if (!selection) return; + const selectedRange = getSelectionAnnotationRange(selection); + if (!selectedRange) return; + + add({ + mentionId: crypto.randomUUID(), + start_char: selectedRange.start, + end_char: selectedRange.end, + paragraphId: selectedRange.paragraphId, + text: selectedRange.text, + attrs: { + aymurai_label: label, + aymurai_label_subclass: null, + aymurai_alt_text: null, + aymurai_alt_start_char: selectedRange.start, + aymurai_alt_end_char: selectedRange.end, + }, + }); + }; + + const handleResolveManualEntity = (resolution: ManualEntityResolution) => { + if (!pendingResolution) return; + + if (resolution.type === "existing") { + commitPredictions( + pendingResolution.predictions, + resolution.candidate.group.canonicalId, + resolution.candidate.group.renderBase as AllLabels | AllLabelsWithSufix, + ); + } else { + commitNewGroup( + pendingResolution.predictions, + pendingResolution.selectedLabel, + ); + } + + setPendingResolution(null); + }; + + return ( + +
{children}
+ setPendingResolution(null)} + onResolve={handleResolveManualEntity} + /> +
+ ); +} + +export const useAnnotation = () => { + const { + add, + remove, + removeByText, + isAnnotable, + label, + updateLabel, + updateByText, + updateByCanonicalId, + addBySearch, + } = useContext(AnnotationContext); + + const createAnnotationData = ( + text: string, + annotation: LabelAnnotation | SearchAnnotation, + labelOverride?: AllLabels | AllLabelsWithSufix, + ) => { + const { start, end, paragraphId, tag } = annotation; + const resolvedTag = labelOverride ?? tag; + if (!resolvedTag) return null; + return { + mentionId: crypto.randomUUID(), + text, + start_char: start, + end_char: end, + paragraphId: paragraphId, + attrs: { + aymurai_label: resolvedTag, + aymurai_label_subclass: null, + aymurai_alt_text: null, + aymurai_alt_start_char: start, + aymurai_alt_end_char: end, + }, + } satisfies PredictLabel; + }; + + return { + add, + remove, + removeByText, + isAnnotable, + label, + updateLabel, + updateByText, + updateByCanonicalId, + addBySearch, + createAnnotationData, + }; +}; diff --git a/frontend/src/renderer/src/context/Annotation/utils.ts b/frontend/src/renderer/src/context/Annotation/utils.ts new file mode 100644 index 00000000..00ddaea4 --- /dev/null +++ b/frontend/src/renderer/src/context/Annotation/utils.ts @@ -0,0 +1,136 @@ +/** + * Check if the selection contains another Node. + * @param selection The current selection. + * @returns `true` if the selection contains a Node, `false` otherwise. + */ +export const selectionHasNodes = (selection: Selection | null): boolean => { + if (!selection) return false; + + const container = document.createElement("div"); + + for (let i = 0; i < selection.rangeCount; ++i) { + container.appendChild(selection.getRangeAt(i).cloneContents()); + } + + return new RegExp(/<.*?>/g).test(container.innerHTML); +}; + +/** + * Checks if the node is can be annotated. It checks the type of Node and its parentElement (if it's a span). + * @param node Node from the selection + * @returns `true` if the node is a valid node, `false` otherwise. + */ +export const isValidNode = (node: Node | null): node is Node => + !!node && + node instanceof Text && + node.parentElement instanceof HTMLSpanElement; + +/** + * Get the start and end boundaries of the selection. Reverses the boundaries if the selection is made from right to left. + * @param selection Selection object + * @returns The start and end boundaries of the selection + */ +export const getBoundaries = (selection: Selection): [number, number] => { + const start = selection.anchorOffset; + const end = selection.focusOffset; + return [start, end].sort((a, b) => a - b) as [number, number]; +}; + +/** + * Get the id of the paragraph where the selection is made. + * @param selection Selection object + * @returns The id of the paragraph where the selection is made + */ +export const paragraphIdFromSelection = (selection: Selection) => { + // This is evaluated after the isValidNode check, so we can safely assume that the parentElement is a span + const span = selection.anchorNode?.parentElement; + const paragraph = span?.parentElement as HTMLParagraphElement; + + return paragraph.id; +}; + +function closestOffsetElement(node: Node | null): HTMLElement | null { + const element = node instanceof HTMLElement ? node : node?.parentElement; + return element?.closest("[data-start]") ?? null; +} + +function offsetWithinElement( + element: HTMLElement, + container: Node, + offset: number, +): number | null { + const range = document.createRange(); + range.setStart(element, 0); + + try { + range.setEnd(container, offset); + } catch { + return null; + } + + return range.toString().length; +} + +export function getSelectionAnnotationRange(selection: Selection): { + paragraphId: string; + start: number; + end: number; + text: string; +} | null { + if (selection.rangeCount === 0 || selection.type !== "Range") return null; + + const range = selection.getRangeAt(0); + const text = range.toString(); + if (!text) return null; + + const startElement = closestOffsetElement(range.startContainer); + const endElement = closestOffsetElement(range.endContainer); + if (!startElement || !endElement) return null; + + const startParagraph = startElement.closest("p[id]"); + const endParagraph = endElement.closest("p[id]"); + if (!startParagraph || !endParagraph || startParagraph.id !== endParagraph.id) + return null; + + const startBase = Number(startElement.dataset.start ?? 0); + const endBase = Number(endElement.dataset.start ?? 0); + const startOffset = offsetWithinElement( + startElement, + range.startContainer, + range.startOffset, + ); + const endOffset = offsetWithinElement( + endElement, + range.endContainer, + range.endOffset, + ); + + if (startOffset === null || endOffset === null) return null; + + return { + paragraphId: startParagraph.id, + start: startBase + startOffset, + end: endBase + endOffset, + text, + }; +} + +/** + * Find all indexes where a search string appears in a text. + * @param text The text to search in + * @param search The string to search for + * @returns An array of indexes where the search string appears + */ +export const findSearchIndexes = (text: string, search: string): number[] => { + const indexes: number[] = []; + const lowerText = text.toLowerCase(); + const lowerSearch = search.toLowerCase(); + let index = lowerText.indexOf(lowerSearch); + + while (index !== -1) { + indexes.push(index); + index = lowerText.indexOf(lowerSearch, index + 1); + } + + return indexes; +}; diff --git a/frontend/src/renderer/src/context/File.tsx b/frontend/src/renderer/src/context/File.tsx new file mode 100644 index 00000000..27d2724d --- /dev/null +++ b/frontend/src/renderer/src/context/File.tsx @@ -0,0 +1,35 @@ +import { + type Dispatch, + type ReactNode, + createContext, + useReducer, +} from "react"; + +import reducer, { type Action } from "@/reducers/file"; +import type { DocFile } from "@/types/file"; + +/** + * Context used to provide files that have to be processed + */ +export const FileContext = createContext([]); +FileContext.displayName = "FileContext"; + +/** + * Context used to provide the dispatch function + */ +export const FileDispatchContext = createContext>(() => {}); +FileDispatchContext.displayName = "FileDispatchContext"; + +interface Props { + children: ReactNode; +} +export default function FileProvider({ children }: Props) { + const [state, dispatch] = useReducer(reducer, []); + return ( + + + {children} + + + ); +} diff --git a/frontend/src/renderer/src/features/APIProtected.tsx b/frontend/src/renderer/src/features/APIProtected.tsx new file mode 100644 index 00000000..0ccd1a76 --- /dev/null +++ b/frontend/src/renderer/src/features/APIProtected.tsx @@ -0,0 +1,12 @@ +import api from "@/services/api"; +import { Navigate } from "@tanstack/react-router"; +import type { ReactNode } from "react"; + +interface Props { + children: ReactNode; +} + +export default function APIProtected({ children }: Props) { + if (!api.defaults.baseURL) return ; + return children; +} diff --git a/frontend/src/renderer/src/features/ReactQueryProvider.tsx b/frontend/src/renderer/src/features/ReactQueryProvider.tsx new file mode 100644 index 00000000..33c23f18 --- /dev/null +++ b/frontend/src/renderer/src/features/ReactQueryProvider.tsx @@ -0,0 +1,55 @@ +import { + MutationCache, + QueryClient, + QueryClientProvider, + matchQuery, +} from "@tanstack/react-query"; + +export function getContext() { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry: false, + }, + }, + mutationCache: new MutationCache({ + onSettled: async (_data, _err, _vars, _ctx, mutation) => { + if (!mutation.meta) return; + const { invalidates, awaits } = mutation.meta; + + const promise = queryClient.invalidateQueries({ + predicate: (query) => { + if (typeof invalidates === "boolean") return invalidates; + + return ( + invalidates?.some((queryKey) => + matchQuery({ queryKey }, query), + ) ?? false + ); + }, + }); + + if (awaits) { + await promise; + } + }, + }), + }); + + return { + queryClient, + }; +} + +export function Provider({ + children, + queryClient, +}: { + children: React.ReactNode; + queryClient: QueryClient; +}) { + return ( + {children} + ); +} diff --git a/frontend/src/renderer/src/features/RequireFile.tsx b/frontend/src/renderer/src/features/RequireFile.tsx new file mode 100644 index 00000000..4e9c518f --- /dev/null +++ b/frontend/src/renderer/src/features/RequireFile.tsx @@ -0,0 +1,22 @@ +import { useFiles } from "@/hooks"; +import { getFeatureRouteSlug, parseFeatureRouteSlug } from "@/types/features"; +import { Navigate, useParams } from "@tanstack/react-router"; +import type { ReactNode } from "react"; + +interface Props { + children: ReactNode; +} + +export default function RequireFile({ children }: Props) { + const files = useFiles(); + const { feature: featureSlug } = useParams({ strict: false }) as { feature?: string }; + const feature = featureSlug ? parseFeatureRouteSlug(featureSlug) : null; + + if (!feature) return ; + + if (!files.length) { + return ; + } + + return children; +} diff --git a/frontend/src/renderer/src/features/showToast.tsx b/frontend/src/renderer/src/features/showToast.tsx new file mode 100644 index 00000000..0199ac4a --- /dev/null +++ b/frontend/src/renderer/src/features/showToast.tsx @@ -0,0 +1,7 @@ +import Toast, { type ToastVariant } from "@/components/ui/toast"; +import type { Icon } from "phosphor-react"; +import { toast } from "react-hot-toast"; + +export function showToast(message: string, variant: ToastVariant = "info", icon?: Icon) { + toast.custom((t) => ); +} diff --git a/frontend/src/renderer/src/hooks/index.ts b/frontend/src/renderer/src/hooks/index.ts new file mode 100644 index 00000000..a5b268f4 --- /dev/null +++ b/frontend/src/renderer/src/hooks/index.ts @@ -0,0 +1,8 @@ +import { useFileDispatch, useFiles } from "./useFiles"; +import useForm from "./useForm"; + +export { useEntityGroups } from "./useEntityGroups"; +export type { EntityGroup } from "./useEntityGroups"; +export { usePredict } from "./usePredict"; + +export { useFileDispatch, useFiles, useForm }; diff --git a/frontend/src/renderer/src/hooks/useDisambiguate.ts b/frontend/src/renderer/src/hooks/useDisambiguate.ts new file mode 100644 index 00000000..b4ce9704 --- /dev/null +++ b/frontend/src/renderer/src/hooks/useDisambiguate.ts @@ -0,0 +1,81 @@ +import { useEffect, useRef } from "react"; + +import { useFileDispatch } from "@/hooks"; +import { addPredictions, removePredictions } from "@/reducers/file/actions"; +import { disambiguate } from "@/services/aymurai/queries"; +import type { DocFile } from "@/types/file"; +import { useQueries } from "@tanstack/react-query"; + +import type { PredictStatus } from "./usePredict"; + +type FileDisambiguate = { status: PredictStatus }; + +/** + * Runs disambiguation for each file **after all its predictions have arrived**. + * When `enabled` is false (e.g. datapublic flow) every file resolves to + * `"completed"` immediately and no requests are made. + * + * Aligned index-wise with `files`: `queries[i]` corresponds to `files[i]`. + */ +export function useDisambiguate( + files: DocFile[], + predictStatuses: Record< + string, + { status: PredictStatus; fromValidation?: boolean } + >, + enabled: boolean, +): Record { + const dispatch = useFileDispatch(); + // Track which files have already had their disambiguated predictions dispatched + const dispatchedRef = useRef(new Set()); + + const queries = useQueries({ + queries: files.map((file) => ({ + ...disambiguate(file), + // Fire only when this step is enabled, predict is fully done, AND the + // predictions were not loaded from stored DB validation (which is already + // post-disambiguation data and must not be overwritten). + enabled: + enabled && + predictStatuses[file.data.name]?.status === "completed" && + !predictStatuses[file.data.name]?.fromValidation, + })), + }); + + // Dispatch disambiguated predictions exactly once per file on success + useEffect(() => { + queries.forEach((query, i) => { + const fileName = files[i].data.name; + if (!query.isSuccess || dispatchedRef.current.has(fileName)) return; + + dispatchedRef.current.add(fileName); + // Replace raw predictions with the disambiguated ones + dispatch(removePredictions(fileName)); + dispatch(addPredictions(fileName, query.data)); + }); + }); + + const result: Record = {}; + + files.forEach((file, i) => { + const fileName = file.data.name; + const query = queries[i]; + const predictDone = predictStatuses[fileName]?.status === "completed"; + + result[fileName] = { + status: !enabled + ? "completed" // this step is skipped for non-anonymizer flows + : predictStatuses[fileName]?.fromValidation + ? "completed" // predictions came from DB validation; disambiguation not needed + : !predictDone + ? "processing" // still waiting on predict + : query.isError + ? "error" + : query.isSuccess + ? "completed" + : "processing", + }; + }); + + return result; +} diff --git a/frontend/src/renderer/src/hooks/useEntityGroups.ts b/frontend/src/renderer/src/hooks/useEntityGroups.ts new file mode 100644 index 00000000..fcdf99b8 --- /dev/null +++ b/frontend/src/renderer/src/hooks/useEntityGroups.ts @@ -0,0 +1,269 @@ +import { useMemo } from "react"; + +import type { PredictLabel } from "@/types/aymurai"; +import type { DocFile } from "@/types/file"; +import { + normalizeEntityText, + stripEntityLabelSuffix, +} from "@/utils/anonymizer/entity-similarity"; + +// ─── Public types ───────────────────────────────────────────────────────────── + +export interface EntityGroup { + /** The canonical_entity_id that identifies this group. */ + canonicalId: string; + /** aymurai_label with numeric suffix stripped (e.g. "PER", "DNI"). */ + renderBase: string; + /** 1-indexed position among all groups sharing the same renderBase, ordered + * by first appearance (paragraphIndex then start_char). */ + suffixIndex: number; + /** Human-readable render token shown to the user (e.g. "PER_1"). */ + renderToken: string; + /** All individual mention predictions belonging to this group. */ + mentions: PredictLabel[]; + /** Deduplicated normalised texts, sorted alphabetically. Used internally for matching. */ + uniqueTexts: string[]; + /** Verbatim texts corresponding 1-to-1 with {@link uniqueTexts}. Each inner array holds all distinct + * surface forms that share the same normalised text (e.g. ["PUERTO BELGRANO", "Puerto Belgrano"]). + * Use for display; use {@link uniqueTexts} for matching/removal. */ + displayTexts: string[][]; + /** Location of the earliest mention, for ordering and UX. */ + firstAppearance: { paragraphIndex: number; start_char: number }; + /** Whether the group should be anonymised (majority vote from mentions). */ + anonymize: boolean; + /** True when another group has the same renderBase and uniqueTexts set. */ + isDuplicate: boolean; + /** canonicalId of the earliest group that is identical to this one. */ + duplicateOf: string | null; +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +/** Strip a numeric suffix like `_1`, `_12` from a label string. */ +export function stripSuffix(label: string): string { + return stripEntityLabelSuffix(label); +} + +// ─── Main hook ──────────────────────────────────────────────────────────────── + +/** + * Derives a flat array of {@link EntityGroup} objects from all predictions + * across the provided files. + * + * Groups are identified by `canonical_entity_id`. Mentions without a + * canonical id are silently skipped (they are raw predict results that haven't + * been through the disambiguate step yet). + * + * Suffix indices are assigned by first-appearance order per `renderBase`. + * Duplicate detection compares `(renderBase, uniqueTexts)`. + */ +export function useEntityGroups(files: DocFile[]): EntityGroup[] { + return useMemo(() => { + // Build a paragraphId → index map across all files so we can order + // mentions by paragraph position. + const paragraphIndexByParagraphId = new Map(); + for (const file of files) { + for (const [i, p] of (file.paragraphs ?? []).entries()) { + if (!paragraphIndexByParagraphId.has(p.id)) { + paragraphIndexByParagraphId.set(p.id, i); + } + } + } + + // Accumulate mentions per canonicalId, plus track renderBase and first appearance. + const byCanonicalId = new Map< + string, + { + mentions: PredictLabel[]; + renderBase: string; + firstAppearance: { paragraphIndex: number; start_char: number }; + } + >(); + + for (const file of files) { + for (const pred of file.predictions ?? []) { + const { canonical_entity_id, aymurai_label } = pred.attrs; + if (!canonical_entity_id) continue; + + const renderBase = stripSuffix(String(aymurai_label)); + const paragraphIndex = + paragraphIndexByParagraphId.get(pred.paragraphId) ?? 0; + + if (!byCanonicalId.has(canonical_entity_id)) { + byCanonicalId.set(canonical_entity_id, { + mentions: [], + renderBase, + firstAppearance: { + paragraphIndex, + start_char: pred.start_char, + }, + }); + } + + const entry = byCanonicalId.get(canonical_entity_id); + if (!entry) continue; + entry.mentions.push(pred); + + // Keep the earliest appearance + const fa = entry.firstAppearance; + if ( + paragraphIndex < fa.paragraphIndex || + (paragraphIndex === fa.paragraphIndex && + pred.start_char < fa.start_char) + ) { + fa.paragraphIndex = paragraphIndex; + fa.start_char = pred.start_char; + } + } + } + + // Sort canonicalIds by first appearance so suffix numbering is stable and + // matches the document reading order (same as backend behaviour). + const sorted = [...byCanonicalId.entries()].sort(([, a], [, b]) => { + const aPara = a.firstAppearance.paragraphIndex; + const bPara = b.firstAppearance.paragraphIndex; + if (aPara !== bPara) return aPara - bPara; + return a.firstAppearance.start_char - b.firstAppearance.start_char; + }); + + // Assign suffix indices per renderBase. + const suffixCounter = new Map(); + const suffixByCanonicalId = new Map(); + for (const [canonicalId, { renderBase }] of sorted) { + const next = (suffixCounter.get(renderBase) ?? 0) + 1; + suffixCounter.set(renderBase, next); + suffixByCanonicalId.set(canonicalId, next); + } + + // Build EntityGroup objects. + const groups: EntityGroup[] = sorted.map( + ([canonicalId, { mentions, renderBase, firstAppearance }]) => { + // Build normalised → first distinct verbatim form seen for that normalisation. + // We intentionally keep only ONE display representative per normalised bucket: + // multiple document spans may share the same alias and we don't want to show + // the same alias chip multiple times. Verbatim comparison uses NFC so that + // Unicode-composed and decomposed forms of the same visible character are + // treated as identical. + const normalizedToVerbatims = new Map(); + for (const m of mentions) { + const norm = normalizeEntityText(m.text); + if (!norm) continue; + const existing = normalizedToVerbatims.get(norm); + if (!existing) { + normalizedToVerbatims.set(norm, [m.text]); + } else if ( + !existing.some( + (v) => + v.normalize("NFC") === m.text.normalize("NFC"), + ) + ) { + existing.push(m.text); + } + } + const uniqueTexts = [...normalizedToVerbatims.keys()].sort(); + const displayTexts = uniqueTexts.map( + (t) => normalizedToVerbatims.get(t) ?? [], + ); + + const anonymizeCount = mentions.filter( + (m) => m.attrs.aymurai_anonymize !== false, + ).length; + const anonymize = anonymizeCount >= mentions.length / 2; + + const suffixIndex = suffixByCanonicalId.get(canonicalId) ?? 1; + + return { + canonicalId, + renderBase, + suffixIndex, + renderToken: `${renderBase}_${suffixIndex}`, + mentions, + uniqueTexts, + displayTexts, + firstAppearance, + anonymize, + isDuplicate: false, + duplicateOf: null, + }; + }, + ); + + // Duplicate / overlap detection: any two groups under the same renderBase + // that share at least one normalised mention text are flagged as merge + // candidates. + // + // The detection key uses a SORTED-TOKEN fingerprint of the normalised text + // so that name variants differing only in token order + // (e.g. "Pérez, Laura Beatriz" vs "Laura Beatriz Pérez") are recognised as + // the same alias and trigger a merge suggestion. + // + // Primary rule: the group with MORE unique texts is primary (it's more + // "established"). Ties are broken by first-appearance order (already the + // iteration order). When a later group turns out to have more texts than + // the registered primary, they swap roles. + + /** Sorted-token fingerprint — collapses token-order variants of a name. */ + const sortedFingerprint = (normText: string): string => + [...new Set(normText.split(" "))].sort().join(" "); + + const textToGroup = new Map(); // key → primary canonicalId + const groupById = new Map(groups.map((g) => [g.canonicalId, g])); + + for (const group of groups) { + let conflictPrimaryId: string | null = null; + + for (const normText of group.uniqueTexts) { + const key = `${group.renderBase}\x01${sortedFingerprint(normText)}`; + const existing = textToGroup.get(key); + if (existing && existing !== group.canonicalId) { + conflictPrimaryId = existing; + break; + } + } + + if (conflictPrimaryId !== null) { + const primaryGroup = groupById.get(conflictPrimaryId); + if (!primaryGroup) continue; + + if (group.uniqueTexts.length > primaryGroup.uniqueTexts.length) { + // Current group has more texts → it becomes the new primary; + // old primary is demoted to duplicate. + primaryGroup.isDuplicate = true; + primaryGroup.duplicateOf = group.canonicalId; + // Re-register all previously-registered keys under new primary. + for (const normText of primaryGroup.uniqueTexts) { + textToGroup.set( + `${primaryGroup.renderBase}\x01${sortedFingerprint(normText)}`, + group.canonicalId, + ); + } + for (const normText of group.uniqueTexts) { + textToGroup.set( + `${group.renderBase}\x01${sortedFingerprint(normText)}`, + group.canonicalId, + ); + } + } else { + // Existing primary has equal or more texts → current is duplicate. + group.isDuplicate = true; + group.duplicateOf = conflictPrimaryId; + // Register any new texts under the existing primary. + for (const normText of group.uniqueTexts) { + const key = `${group.renderBase}\x01${sortedFingerprint(normText)}`; + if (!textToGroup.has(key)) textToGroup.set(key, conflictPrimaryId); + } + } + } else { + // No conflict — register all own texts as primary. + for (const normText of group.uniqueTexts) { + textToGroup.set( + `${group.renderBase}\x01${sortedFingerprint(normText)}`, + group.canonicalId, + ); + } + } + } + + return groups; + }, [files]); +} diff --git a/frontend/src/renderer/src/hooks/useFileParse.ts b/frontend/src/renderer/src/hooks/useFileParse.ts new file mode 100644 index 00000000..a45da857 --- /dev/null +++ b/frontend/src/renderer/src/hooks/useFileParse.ts @@ -0,0 +1,69 @@ +import { useEffect, useState } from "react"; + +import { useFileDispatch } from "@/hooks"; +import { addParagraphs } from "@/reducers/file/actions"; +import { fileParser } from "@/services/aymurai/queries"; +import type { DocFile } from "@/types/file"; +import { useQueries, useQueryClient } from "@tanstack/react-query"; + +import type { PredictStatus } from "./usePredict"; + +export function useFileParse( + files: DocFile[], +): Record void }> { + const dispatch = useFileDispatch(); + const queryClient = useQueryClient(); + const [abortedFiles, setAbortedFiles] = useState>(new Set()); + + const queries = useQueries({ + queries: files.map((file) => ({ + ...fileParser(file.data), + enabled: !abortedFiles.has(file.data.name), + })), + }); + + useEffect(() => { + queries.forEach((query, fileIndex) => { + const file = files[fileIndex]; + // Use the Redux state as the guard: if paragraphs are already set, skip. + // This correctly re-dispatches when the same file is reloaded after a replace. + if (!query.isSuccess || !query.data || file.paragraphs !== undefined) + return; + dispatch( + addParagraphs( + query.data.document.map((p, paragraphIndex) => ({ + value: p, + document_id: query.data.document_id, + id: `${query.data.document_id}:${paragraphIndex}`, + })), + file.data.name, + ), + ); + }); + }); + + const result: Record void }> = + {}; + files.forEach((file, i) => { + const q = queries[i]; + const fileName = file.data.name; + const isAborted = abortedFiles.has(fileName); + result[fileName] = { + status: isAborted + ? "stopped" + : q.isError + ? "error" + : q.isSuccess + ? "completed" + : "processing", + abort: () => { + queryClient.cancelQueries({ + queryKey: ["file-parser", fileName], + exact: false, + }); + setAbortedFiles((prev) => new Set([...prev, fileName])); + }, + }; + }); + return result; +} diff --git a/frontend/src/renderer/src/hooks/useFiles.ts b/frontend/src/renderer/src/hooks/useFiles.ts new file mode 100644 index 00000000..280012e0 --- /dev/null +++ b/frontend/src/renderer/src/hooks/useFiles.ts @@ -0,0 +1,17 @@ +import { useContext } from "react"; + +import { FileContext, FileDispatchContext } from "@/context/File"; + +/** + * Hook used to view the files in the state + */ +export function useFiles() { + return useContext(FileContext); +} + +/** + * Hook used to dispatch actions that modify the state + */ +export function useFileDispatch() { + return useContext(FileDispatchContext); +} diff --git a/frontend/src/renderer/src/hooks/useForm/index.ts b/frontend/src/renderer/src/hooks/useForm/index.ts new file mode 100644 index 00000000..5dacc409 --- /dev/null +++ b/frontend/src/renderer/src/hooks/useForm/index.ts @@ -0,0 +1,83 @@ +import { LabelDecisiones, type LabelType } from "@/types/aymurai"; +import nArray from "@/utils/nArray"; +import { useRef, useState } from "react"; +import type { FormData, RegisterFunction, SubmitFunction } from "./types"; + +/** + * Creates a group of component references and a function to handle + * @param initialDecisiones Initial amount of decisiones + * @returns `register()` and `submit()` functions + */ +export default function useForm(initialDecisiones = 1) { + // Create an n-array of empty objects + const refs = useRef({ DECISIONES: nArray(initialDecisiones, {}) }); + // State containing decisionAmounts. This is done in this way to make React perform a re-render + // in case we want to add a decision + const [decisionAmount, setDecisionAmount] = useState(initialDecisiones); + + /** + * Adds a reference to a components value to the 'state' + * @param name Name of the reference + * @returns A `(ref) => void` function that should be applied to the `ref={...}` prop of a component + */ + const register: RegisterFunction = (name, decision) => (ref) => { + if (ref) { + // We have to alter the DECISIONES array + if (name in LabelDecisiones) { + // In case we already have an array, modify the label + if (refs.current.DECISIONES) { + const arr = refs.current.DECISIONES; + const i = decision ?? 0; + + arr[i][name as LabelDecisiones] = ref.value; + } else { + // We have no array, create it + refs.current.DECISIONES = [{ [name]: ref.value }]; + } + } else { + // Just modify the label in the object + refs.current[name as LabelType] = ref.value; + } + } + }; + + /** + * Collects all the data from the referrences and creates a submit function with them + * @param callback Callback that is called with all the data collected from the references as an argument + * @returns A `(event) => void` function that should be applied to the `onSubmit={...}` prop of a form + */ + const submit = (callback: SubmitFunction) => () => { + callback(refs.current); + }; + + const addDecision = () => { + const arr = refs.current.DECISIONES; + + if (arr) { + setDecisionAmount((dec) => dec + 1); + refs.current.DECISIONES = [...arr, {}]; + } else { + setDecisionAmount(1); + refs.current.DECISIONES = [{}]; + } + }; + + const getDecisionValue = (n: number) => (field: LabelDecisiones) => { + const arr = refs.current.DECISIONES; + + if (arr && n in arr && field in arr[n]) { + return arr[n][field]; + } + return ""; + }; + + return { + register, + submit, + addDecision, + decisionAmount, + getDecisionValue, + }; +} + +export * from "./types"; diff --git a/frontend/src/renderer/src/hooks/useForm/types.ts b/frontend/src/renderer/src/hooks/useForm/types.ts new file mode 100644 index 00000000..3832a118 --- /dev/null +++ b/frontend/src/renderer/src/hooks/useForm/types.ts @@ -0,0 +1,36 @@ +import type { + AllLabels, + AllLabelsWithSufix, + LabelDecisiones, + LabelType, +} from "@/types/aymurai"; + +/** + * Any value the form can take + */ +export type FormValue = string | boolean | undefined; +/** + * Data structure of the whole form + */ +export type FormData = Partial< + Record & { + DECISIONES: Partial>[]; + } +>; +/** + * Flat form of the `FormData` structure + */ +export type FlatFormData = Partial< + Record +>; + +/** + * Type of the reference for any componente + */ +export type ComponentRef = { value: string | boolean | undefined } | null; + +export type RegisterFunction = ( + name: AllLabels | AllLabelsWithSufix, + decision?: number, +) => (ref: ComponentRef) => void; +export type SubmitFunction = (data: FormData) => void; diff --git a/frontend/src/renderer/src/hooks/usePredict.ts b/frontend/src/renderer/src/hooks/usePredict.ts new file mode 100644 index 00000000..9ab803e2 --- /dev/null +++ b/frontend/src/renderer/src/hooks/usePredict.ts @@ -0,0 +1,171 @@ +import { useRef, useState } from "react"; + +import { useFileDispatch } from "@/hooks"; +import { addPredictions, removePredictions } from "@/reducers/file/actions"; +import predict from "@/services/aymurai/predict"; +import { predictParagraph } from "@/services/aymurai/queries"; +import getStoredValidation from "@/services/aymurai/validation"; +import type { Workflows } from "@/types/aymurai"; +import type { DocFile } from "@/types/file"; +import { useQueries, useQueryClient } from "@tanstack/react-query"; + +// Limit concurrent in-flight predict requests to avoid browser connection exhaustion +const MAX_CONCURRENT = 100; +const semaphore = (() => { + let running = 0; + const queue: Array<() => void> = []; + return { + acquire(): Promise { + return new Promise((resolve) => { + if (running < MAX_CONCURRENT) { + running++; + resolve(); + } else queue.push(resolve); + }); + }, + release() { + const next = queue.shift(); + if (next) next(); + else running = Math.max(0, running - 1); + }, + }; +})(); + +export type PredictStatus = "processing" | "error" | "stopped" | "completed"; + +type FilePredict = { + progress: number; + status: PredictStatus; + abort: () => void; + /** + * True when every paragraph in this file was loaded from the backend's stored + * validation (`anonymization_paragraph.validation`) instead of running the + * model. When true, the disambiguation step must be skipped. + */ + fromValidation: boolean; +}; + +/** + * Runs predictions for all paragraphs across all given files in parallel via + * React Query. Dispatches results to the Redux file store as each paragraph + * resolves. Returns a per-filename map of `{ progress, status, abort }`. + * + * Abort uses `queryClient.cancelQueries` so there are no manual AbortController + * references to track or clean up. + */ +export function usePredict( + files: DocFile[], + workflow: Workflows, +): Record { + const dispatch = useFileDispatch(); + const queryClient = useQueryClient(); + const [abortedFiles, setAbortedFiles] = useState>(new Set()); + + // Paragraph IDs whose labels were loaded from DB validation (not from predict). + // Used to gate the disambiguation step. + const fromValidationRef = useRef>(new Set()); + + // Flatten all (file, paragraph) pairs — one React Query entry each + const pairs = files.flatMap((file) => + (file.paragraphs ?? []).map((paragraph) => ({ file, paragraph })), + ); + + const queries = useQueries({ + queries: pairs.map(({ file, paragraph }) => ({ + // Use the factory for the query key and shared defaults (staleTime, retry) + ...predictParagraph(paragraph, workflow, file.data.name), + enabled: !abortedFiles.has(file.data.name), + // Override queryFn to dispatch to Redux as each paragraph arrives. + // Defined inline to avoid TS's `queryFn?: ...` optionality on the factory type. + queryFn: async ({ signal }: { signal?: AbortSignal }) => { + const controller = new AbortController(); + signal?.addEventListener("abort", () => controller.abort()); + await semaphore.acquire(); + try { + if (!controller.signal.aborted) { + // For anonymizer: check stored DB validation before running the model. + // null → no stored validation; fall through to predict + // [] → explicitly validated with no entities; respect the empty state + // [...] → stored manual annotations; restore them + if (workflow === "anonymizer") { + const stored = await getStoredValidation(paragraph, controller); + if (stored !== null) { + console.debug( + `[predict] Restored ${stored.length} labels from DB validation`, + { paragraphId: paragraph.id.slice(0, 60) }, + ); + fromValidationRef.current.add(paragraph.id); + dispatch(addPredictions(file.data.name, stored)); + return stored; + } + console.debug("[predict] No stored validation — running model predict", { + paragraphId: paragraph.id.slice(0, 60), + }); + } + const predictions = await predict(paragraph, controller, workflow); + dispatch(addPredictions(file.data.name, predictions)); + return predictions; + } + return []; + } finally { + semaphore.release(); + } + }, + })), + }); + + // Build per-file { progress, status, abort } from the flat query results + const result: Record = {}; + + for (const file of files) { + const fileName = file.data.name; + const total = (file.paragraphs ?? []).length; + + // Collect only the queries that belong to this file + const fileQueries = pairs.reduce<(typeof queries)[number][]>( + (acc, pair, i) => { + if (pair.file.data.name === fileName) acc.push(queries[i]); + return acc; + }, + [], + ); + + const successCount = fileQueries.filter((q) => q.isSuccess).length; + const errorCount = fileQueries.filter((q) => q.isError).length; + const progress = total > 0 ? successCount / total : 0; + const isAborted = abortedFiles.has(fileName); + + const status: PredictStatus = isAborted + ? "stopped" + : errorCount > 0 + ? "error" + : total === 0 || successCount === total + ? "completed" + : "processing"; + + // A file is "from validation" when every one of its paragraphs returned + // stored annotations from the backend (none went through model predict). + const fromValidation = + workflow === "anonymizer" && + total > 0 && + (file.paragraphs ?? []).every((p) => fromValidationRef.current.has(p.id)); + + result[fileName] = { + progress, + status, + fromValidation, + abort: () => { + // Cancel all in-flight paragraph queries for this file via RQ's own + // cancellation mechanism — no manual controller refs to clean up + queryClient.cancelQueries({ + queryKey: ["predict", workflow, fileName], + exact: false, + }); + dispatch(removePredictions(fileName)); + setAbortedFiles((prev) => new Set([...prev, fileName])); + }, + }; + } + + return result; +} diff --git a/frontend/src/renderer/src/index.css b/frontend/src/renderer/src/index.css new file mode 100644 index 00000000..e27a23b7 --- /dev/null +++ b/frontend/src/renderer/src/index.css @@ -0,0 +1 @@ +@layer reset, base, tokens, recipes, utilities; diff --git a/frontend/src/renderer/src/layout/home.tsx b/frontend/src/renderer/src/layout/home.tsx new file mode 100644 index 00000000..563d8f80 --- /dev/null +++ b/frontend/src/renderer/src/layout/home.tsx @@ -0,0 +1,50 @@ +import BuiltBy from "@/components/brand/built-by"; +import { css } from "@/styled/css"; + +const background = css({ + display: "flex", + + bg: "bg.primary-highlight", + height: "screen", + width: "screen", + + p: "8", +}); + +const inner = css({ + display: "flex", + flexDirection: "column", + alignItems: "center", + + pos: "relative", + + bg: "bg.secondary", + border: "primary", + + width: "full", + height: "full", + + overflowY: "auto", + pb: "8", +}); + +const childrenArea = css({ + flex: "1", + display: "flex", + alignItems: "center", + justifyContent: "center", +}); + +interface HomeLayoutProps { + children?: React.ReactNode; +} +export default function HomeLayout({ children }: HomeLayoutProps) { + return ( +
+
+
{children}
+ +
+
+ ); +} diff --git a/frontend/src/renderer/src/layout/loading.tsx b/frontend/src/renderer/src/layout/loading.tsx new file mode 100644 index 00000000..1f1a3d39 --- /dev/null +++ b/frontend/src/renderer/src/layout/loading.tsx @@ -0,0 +1,31 @@ +import { css } from "@/styled/css"; +import { stack } from "@/styled/patterns"; + +const background = css( + stack.raw({ + align: "center", + justify: "center", + }), + { + width: "screen", + height: "screen", + + bgGradient: "primary", + }, +); +const image = css({ + animation: "pulse", +}); + +export default function Loading() { + return ( +
+ AymurAI iso white +
+ ); +} diff --git a/frontend/src/renderer/src/layout/section-title.tsx b/frontend/src/renderer/src/layout/section-title.tsx new file mode 100644 index 00000000..26731e61 --- /dev/null +++ b/frontend/src/renderer/src/layout/section-title.tsx @@ -0,0 +1,13 @@ +import { styled } from "@/styled/jsx"; + +interface SectionTitleProps { + children: React.ReactNode; + className?: string; +} +export function SectionTitle({ children, className }: SectionTitleProps) { + return ( + + {children} + + ); +} diff --git a/frontend/src/renderer/src/main.tsx b/frontend/src/renderer/src/main.tsx new file mode 100644 index 00000000..9f8c88be --- /dev/null +++ b/frontend/src/renderer/src/main.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; + +import "./index.css"; +import "./constants/i18n"; +import App from "./app"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/frontend/src/renderer/src/reducers/file/actions.ts b/frontend/src/renderer/src/reducers/file/actions.ts new file mode 100644 index 00000000..331a2edb --- /dev/null +++ b/frontend/src/renderer/src/reducers/file/actions.ts @@ -0,0 +1,454 @@ +import type { FormData } from "@/hooks/useForm"; +import type { + AllLabels, + AllLabelsWithSufix, + PredictLabel, +} from "@/types/aymurai"; +import type { Paragraph } from "@/types/file"; + +/** + * List of action types. + */ +export enum ActionTypes { + ADD = "ADD", + ADD_PREDICTIONS = "ADD_PREDICTIONS", + ADD_PARAGRAPHS = "ADD_PARAGRAPHS", + FILTER_UNSELECTED = "FILTER_UNSELECTED", + FILTER_UNPROCESSED = "FILTER_UNPROCESSED", + TOGGLE_SELECTED = "TOGGLE_SELECTED", + REMOVE_ALL_PREDICTIONS = "REMOVE_ALL_PREDICTIONS", + REMOVE_PREDICTIONS = "REMOVE_PREDICTIONS", + REMOVE_ALL_FILES = "REMOVE_ALL_FILES", + REMOVE_FILE = "REMOVE_FILE", + REPLACE_FILE = "REPLACE_FILE", + VALIDATE = "VALIDATE", + APPEND_VALIDATION = "APPEND_VALIDATION", + APPEND_PREDICTION = "APPEND_PREDICTION", + REMOVE_PREDICTION = "REMOVE_PREDICTION", + REMOVE_PREDICTIONS_BY_TEXT = "REMOVE_PREDICTIONS_BY_TEXT", + UPDATE_PREDICTION_LABEL = "UPDATE_PREDICTION_LABEL", + UPDATE_PREDICTIONS_BY_TEXT = "UPDATE_PREDICTIONS_BY_TEXT", + REMOVE_PREDICTIONS_BY_CANONICAL_ID = "REMOVE_PREDICTIONS_BY_CANONICAL_ID", + REMOVE_PREDICTION_VALUE_BY_CANONICAL_ID = "REMOVE_PREDICTION_VALUE_BY_CANONICAL_ID", + UPDATE_PREDICTIONS_BY_CANONICAL_ID = "UPDATE_PREDICTIONS_BY_CANONICAL_ID", + MOVE_MENTION_TO_GROUP = "MOVE_MENTION_TO_GROUP", + MERGE_GROUPS = "MERGE_GROUPS", +} + +/** + * Generic action + */ +type Action> = { + type: Type; + payload: Payload; +}; + +export type ToggleSelectedAction = Action< + ActionTypes.TOGGLE_SELECTED, + { fileName: string } +>; +/** + * Toggles the `selected` property of the given file + * @param fileName Name of the file to be toggled + */ +export function toggleSelected(fileName: string): ToggleSelectedAction { + return { + type: ActionTypes.TOGGLE_SELECTED, + payload: { fileName }, + }; +} + +export type RemoveAllPredictionsAction = + Action; +/** + * Removes all the predictions for all the files in the state + */ +export function removeAllPredictions(): RemoveAllPredictionsAction { + return { + type: ActionTypes.REMOVE_ALL_PREDICTIONS, + payload: {}, + }; +} + +export type FilterUnselectedAction = Action; +/** + * Removes files from the state whose `selected` property is set on `false` + */ +export function filterUnselected(): FilterUnselectedAction { + return { + type: ActionTypes.FILTER_UNSELECTED, + payload: {}, + }; +} + +export type AddFilesAction = Action; +/** + * Adds a new set of `File[]` to the end of the state + * @param newFiles `File[]` to be added to the state + */ +export function addFiles(newFiles: File[]): AddFilesAction { + return { + type: ActionTypes.ADD, + payload: { newFiles }, + }; +} + +export type RemoveAllFilesAction = Action; +/** + * Removes all the files from the state + */ +export function removeAllFiles(): RemoveAllFilesAction { + return { + type: ActionTypes.REMOVE_ALL_FILES, + payload: {}, + }; +} + +export type RemoveFileAction = Action< + ActionTypes.REMOVE_FILE, + { fileName: string } +>; +/** + * Removes a single file from the array + */ +export function removeFile(fileName: string): RemoveFileAction { + return { + type: ActionTypes.REMOVE_FILE, + payload: { fileName }, + }; +} + +export type AddPredictionsAction = Action< + ActionTypes.ADD_PREDICTIONS, + { fileName: string; predictions: PredictLabel[] } +>; +/** + * Adds predictions to a specific file + * @param fileName File to be modified + * @param predictions `PredictLabel[]` received from the `/predict` API request + */ +export function addPredictions( + fileName: string, + predictions: PredictLabel[], +): AddPredictionsAction { + return { + type: ActionTypes.ADD_PREDICTIONS, + payload: { fileName, predictions }, + }; +} + +export type RemovePredictionsAction = Action< + ActionTypes.REMOVE_PREDICTIONS, + { fileName: string } +>; +/** + * Removes all the predictions from a given file + */ +export function removePredictions(fileName: string): RemovePredictionsAction { + return { + type: ActionTypes.REMOVE_PREDICTIONS, + payload: { fileName }, + }; +} + +export type ReplaceFileAction = Action< + ActionTypes.REPLACE_FILE, + { fileName: string; file: File } +>; +/** + * Replaces a file data with the given `fileName` + * @param fileName File to be replaced on the state + * @param file File data to be inserted in place of the given `fileName` + */ +export function replaceFile(fileName: string, file: File): ReplaceFileAction { + return { + type: ActionTypes.REPLACE_FILE, + payload: { fileName, file }, + }; +} + +export type ValidateAction = Action; +/** + * Sets the validated status to the given file to `true` + * @param fileName Name of the file to be toggled + */ +export function validate(fileName: string): ValidateAction { + return { + type: ActionTypes.VALIDATE, + payload: { fileName }, + }; +} + +export type AppendValidationAction = Action< + ActionTypes.APPEND_VALIDATION, + { fileName: string; validation: FormData } +>; +/** + * Builds the `validation` progressively + * @param fileName Name of the file to be modified + * @param validation Validation object to be appended + */ +export function appendValidation( + fileName: string, + validation: FormData, +): AppendValidationAction { + return { + type: ActionTypes.APPEND_VALIDATION, + payload: { fileName, validation }, + }; +} + +export type FilterUnprocessedAction = Action; +/** + * Removes files from the state whose `predictions` property is set on `undefined` + */ +export function filterUnprocessed(): FilterUnprocessedAction { + return { + type: ActionTypes.FILTER_UNPROCESSED, + payload: {}, + }; +} + +export type AddParagraphsAction = Action< + ActionTypes.ADD_PARAGRAPHS, + { + paragraphs: Paragraph[]; + fileName: string; + } +>; +/** + * Adds paragraphs to the state from the endpoint `/document-extract` + * @param paragraphs List of paragraphs to be added + * @param fileName Name of the file to be modified + */ +export function addParagraphs( + paragraphs: Paragraph[], + fileName: string, +): AddParagraphsAction { + return { + type: ActionTypes.ADD_PARAGRAPHS, + payload: { paragraphs, fileName }, + }; +} + +export type AppendPrediction = Action< + ActionTypes.APPEND_PREDICTION, + { + prediction: PredictLabel; + fileName: string; + } +>; +/** + * Appends a new user made prediction to the file + * @param prediction User made prediction on the file. Should only be used when anonymizing. + * @param fileName Name of the file to be modified + */ +export function appendPrediction( + fileName: string, + prediction: PredictLabel, +): AppendPrediction { + return { + type: ActionTypes.APPEND_PREDICTION, + payload: { prediction, fileName }, + }; +} + +export type RemovePrediction = Action< + ActionTypes.REMOVE_PREDICTION, + { + prediction: PredictLabel; + fileName: string; + } +>; +/** + * Removes a user made prediction from the file + * @param predictionId ID of the prediction to be removed + * @param fileName Name of the file to be modified + */ +export function removePrediction( + fileName: string, + prediction: PredictLabel, +): RemovePrediction { + return { + type: ActionTypes.REMOVE_PREDICTION, + payload: { prediction, fileName }, + }; +} + +export type RemovePredictionsByText = Action< + ActionTypes.REMOVE_PREDICTIONS_BY_TEXT, + { + text: string; + fileName: string; + } +>; +/** + * Removes all predictions with the same text from the file + * @param text Text of the predictions to be removed + * @param fileName Name of the file to be modified + */ +export function removePredictionsByText( + fileName: string, + text: string, +): RemovePredictionsByText { + return { + type: ActionTypes.REMOVE_PREDICTIONS_BY_TEXT, + payload: { text, fileName }, + }; +} + +export type UpdatePredictionLabel = Action< + ActionTypes.UPDATE_PREDICTION_LABEL, + { + fileName: string; + prediction: PredictLabel; + newLabel: string; + } +>; + +/** + * Updates the label of a prediction in a file + * @param fileName Name of the file containing the prediction + * @param prediction The prediction to update + * @param newLabel The new label to set + */ +export function updatePredictionLabel( + fileName: string, + prediction: PredictLabel, + newLabel: string, +): UpdatePredictionLabel { + return { + type: ActionTypes.UPDATE_PREDICTION_LABEL, + payload: { fileName, prediction, newLabel }, + }; +} + +export type UpdatePredictionsByText = Action< + ActionTypes.UPDATE_PREDICTIONS_BY_TEXT, + { + text: string; + fileName: string; + newLabel: AllLabels | AllLabelsWithSufix; + canonicalId?: string; + } +>; + +/** + * Updates all predictions with the same text to a new label + * @param fileName Name of the file containing the predictions + * @param text Text of the predictions to update + * @param newLabel The new label to set + */ +export function updatePredictionsByText( + fileName: string, + text: string, + newLabel: AllLabels | AllLabelsWithSufix, + canonicalId?: string, +): UpdatePredictionsByText { + return { + type: ActionTypes.UPDATE_PREDICTIONS_BY_TEXT, + payload: { fileName, text, newLabel, canonicalId }, + }; +} + +export type RemovePredictionsByCanonicalId = Action< + ActionTypes.REMOVE_PREDICTIONS_BY_CANONICAL_ID, + { canonicalId: string } +>; +/** + * Removes all predictions across all files that share the given canonical_entity_id + * @param canonicalId The canonical entity id to match + */ +export function removePredictionsByCanonicalId( + canonicalId: string, +): RemovePredictionsByCanonicalId { + return { + type: ActionTypes.REMOVE_PREDICTIONS_BY_CANONICAL_ID, + payload: { canonicalId }, + }; +} + +export type RemovePredictionValueByCanonicalId = Action< + ActionTypes.REMOVE_PREDICTION_VALUE_BY_CANONICAL_ID, + { canonicalId: string; value: string } +>; +/** + * Removes predictions across all files matching the given canonical_entity_id and text value + * @param canonicalId The canonical entity id to match + * @param value The prediction text to remove + */ +export function removePredictionValueByCanonicalId( + canonicalId: string, + value: string, +): RemovePredictionValueByCanonicalId { + return { + type: ActionTypes.REMOVE_PREDICTION_VALUE_BY_CANONICAL_ID, + payload: { canonicalId, value }, + }; +} + +export type UpdatePredictionsByCanonicalId = Action< + ActionTypes.UPDATE_PREDICTIONS_BY_CANONICAL_ID, + { canonicalId: string; newLabel: AllLabels | AllLabelsWithSufix } +>; +/** + * Updates the label of all predictions across all files sharing the given canonical_entity_id + * @param canonicalId The canonical entity id to match + * @param newLabel The new label to set + */ +export function updatePredictionsByCanonicalId( + canonicalId: string, + newLabel: AllLabels | AllLabelsWithSufix, +): UpdatePredictionsByCanonicalId { + return { + type: ActionTypes.UPDATE_PREDICTIONS_BY_CANONICAL_ID, + payload: { canonicalId, newLabel }, + }; +} + +export type MoveMentionToGroupAction = Action< + ActionTypes.MOVE_MENTION_TO_GROUP, + { + mentionId: string; + targetCanonicalId: string; + targetLabel: AllLabels | AllLabelsWithSufix; + } +>; +/** + * Moves a single mention (identified by its stable mentionId) to a different + * canonical entity group, updating both `canonical_entity_id` and `aymurai_label`. + */ +export function moveMentionToGroup( + mentionId: string, + targetCanonicalId: string, + targetLabel: AllLabels | AllLabelsWithSufix, +): MoveMentionToGroupAction { + return { + type: ActionTypes.MOVE_MENTION_TO_GROUP, + payload: { mentionId, targetCanonicalId, targetLabel }, + }; +} + +export type MergeGroupsAction = Action< + ActionTypes.MERGE_GROUPS, + { + sourceCanonicalId: string; + targetCanonicalId: string; + targetLabel: AllLabels | AllLabelsWithSufix; + } +>; +/** + * Moves all mentions from `sourceCanonicalId` into `targetCanonicalId`, + * updating their label to `targetLabel`. The source group disappears naturally + * once it has no more mentions. + */ +export function mergeGroups( + sourceCanonicalId: string, + targetCanonicalId: string, + targetLabel: AllLabels | AllLabelsWithSufix, +): MergeGroupsAction { + return { + type: ActionTypes.MERGE_GROUPS, + payload: { sourceCanonicalId, targetCanonicalId, targetLabel }, + }; +} diff --git a/frontend/src/renderer/src/reducers/file/index.ts b/frontend/src/renderer/src/reducers/file/index.ts new file mode 100644 index 00000000..d92c80bb --- /dev/null +++ b/frontend/src/renderer/src/reducers/file/index.ts @@ -0,0 +1,415 @@ +import { + ActionTypes, + type AddFilesAction, + type AddParagraphsAction, + type AddPredictionsAction, + type AppendPrediction, + type AppendValidationAction, + type FilterUnprocessedAction, + type FilterUnselectedAction, + type MergeGroupsAction, + type MoveMentionToGroupAction, + type RemoveAllFilesAction, + type RemoveAllPredictionsAction, + type RemoveFileAction, + type RemovePrediction, + type RemovePredictionValueByCanonicalId, + type RemovePredictionsAction, + type RemovePredictionsByCanonicalId, + type RemovePredictionsByText, + type ReplaceFileAction, + type ToggleSelectedAction, + type UpdatePredictionLabel, + type UpdatePredictionsByCanonicalId, + type UpdatePredictionsByText, + type ValidateAction, +} from "./actions"; +import { + addFiles, + comparePrediction, + replaceFile, + updateFromState, +} from "./utils"; + +import type { AllLabels, AllLabelsWithSufix } from "@/types/aymurai"; +import type { DocFile } from "@/types/file"; +import { + normalizeEntityText, + stripEntityLabelSuffix, +} from "@/utils/anonymizer/entity-similarity"; + +type State = DocFile[]; + +export type Action = + | AddFilesAction + | AddPredictionsAction + | ToggleSelectedAction + | RemoveAllPredictionsAction + | RemoveAllFilesAction + | RemoveFileAction + | RemovePredictionsAction + | ReplaceFileAction + | FilterUnselectedAction + | ValidateAction + | AppendValidationAction + | FilterUnprocessedAction + | AddParagraphsAction + | AppendPrediction + | RemovePrediction + | RemovePredictionsByText + | UpdatePredictionLabel + | UpdatePredictionsByText + | RemovePredictionsByCanonicalId + | RemovePredictionValueByCanonicalId + | UpdatePredictionsByCanonicalId + | MoveMentionToGroupAction + | MergeGroupsAction; + +/** + * Reducer function for `DocFile[]` state + * @param state Current state + * @param action Action to perform + * @returns A new state + */ +export default function reducer(state: State, action: Action): State { + const { type, payload } = action; + + // Update/replace/delete utility function + const update = updateFromState(state); + + switch (type) { + // ---------------- + // ADD + // ---------------- + case ActionTypes.ADD: { + const { newFiles } = payload; + + // Performs some checkings on the file + return addFiles(newFiles, state); + } + // ---------------- + // ADD PREDICTIONS + // ---------------- + case ActionTypes.ADD_PREDICTIONS: { + const { fileName, predictions } = payload; + + return update(fileName, (current) => ({ + ...current, + // Appends the predictions to the already created array + predictions: [...(current.predictions ?? []), ...predictions], + })); + } + // ---------------- + // TOGGLE SELECTED + // ---------------- + case ActionTypes.TOGGLE_SELECTED: { + const { fileName } = payload; + + return update(fileName, (current) => ({ + ...current, + selected: !current.selected, + })); + } + // ---------------- + // REMOVE ALL PREDICTIONS + // ---------------- + case ActionTypes.REMOVE_ALL_PREDICTIONS: { + return state.map((file) => ({ ...file, predictions: undefined })); + } + // ---------------- + // REMOVE PREDICTIONS + // ---------------- + case ActionTypes.REMOVE_PREDICTIONS: { + const { fileName } = payload; + + return update(fileName, (current) => ({ + ...current, + predictions: undefined, + })); + } + // ---------------- + // REMOVE ALL FILES + // ---------------- + case ActionTypes.REMOVE_ALL_FILES: { + return []; + } + case ActionTypes.REMOVE_FILE: { + const { fileName } = payload; + + return state.filter((file) => file.data.name !== fileName); + } + // ---------------- + // REPLACE FILE + // ---------------- + case ActionTypes.REPLACE_FILE: { + const { fileName, file } = payload; + return replaceFile(fileName, file, state); + } + // ---------------- + // FILTER UNSELECTED + // ---------------- + case ActionTypes.FILTER_UNSELECTED: { + return state.filter((file) => file.selected); + } + + // ---------------- + // FILTER UNSELECTED + // ---------------- + case ActionTypes.FILTER_UNPROCESSED: { + return state.filter((file) => file.predictions); + } + // ---------------- + // VALIDATE + // ---------------- + case ActionTypes.VALIDATE: { + const { fileName } = payload; + return update(fileName, (cur) => ({ ...cur, validated: true })); + } + // ---------------- + // APPEND VALIDATION + // ---------------- + case ActionTypes.APPEND_VALIDATION: { + const { fileName, validation } = payload; + + return update(fileName, (cur) => ({ + ...cur, + // Append to the already created object + validationObject: { ...cur.validationObject, ...validation }, + })); + } + + // ---------------- + // ADD PARAGRAPHS + // ---------------- + case ActionTypes.ADD_PARAGRAPHS: { + const { fileName, paragraphs } = payload; + + return update(fileName, (cur) => ({ + ...cur, + paragraphs: paragraphs, + })); + } + + // ---------------- + // APPEND PREDICTION + // ---------------- + case ActionTypes.APPEND_PREDICTION: { + const { fileName, prediction } = payload; + + return update(fileName, (cur) => ({ + ...cur, + predictions: [...(cur.predictions ?? []), prediction], + })); + } + + // ---------------- + // REMOVE PREDICTION + // ---------------- + case ActionTypes.REMOVE_PREDICTION: { + const { fileName, prediction } = payload; + + return update(fileName, (cur) => ({ + ...cur, + predictions: cur.predictions?.filter( + (p) => !comparePrediction(prediction, p), + ), + })); + } + + // ---------------- + // REMOVE PREDICTIONS BY TEXT + // ---------------- + case ActionTypes.REMOVE_PREDICTIONS_BY_TEXT: { + const { fileName, text } = payload; + const normalizedText = normalizeEntityText(text); + + return update(fileName, (cur) => ({ + ...cur, + predictions: cur.predictions?.filter( + (p) => normalizeEntityText(p.text) !== normalizedText, + ), + })); + } + + // ---------------- + // UPDATE PREDICTION LABEL + // ---------------- + case ActionTypes.UPDATE_PREDICTION_LABEL: { + const { fileName, prediction, newLabel } = action.payload; + return state.map((file) => { + if (file.data.name !== fileName) return file; + + return { + ...file, + predictions: file.predictions?.map((p) => { + const isSamePrediction = prediction.mentionId + ? p.mentionId === prediction.mentionId + : p.paragraphId === prediction.paragraphId && + p.text === prediction.text && + p.start_char === prediction.start_char && + p.end_char === prediction.end_char; + + if (isSamePrediction) { + const currentBaseLabel = stripEntityLabelSuffix( + String(p.attrs.aymurai_label), + ); + const nextBaseLabel = stripEntityLabelSuffix(String(newLabel)); + const canonicalPatch = + currentBaseLabel === nextBaseLabel + ? {} + : { canonical_entity_id: crypto.randomUUID() }; + + return { + ...p, + attrs: { + ...p.attrs, + aymurai_label: newLabel as AllLabels | AllLabelsWithSufix, + ...canonicalPatch, + }, + }; + } + return p; + }), + }; + }); + } + + // ---------------- + // UPDATE PREDICTIONS BY TEXT + // ---------------- + case ActionTypes.UPDATE_PREDICTIONS_BY_TEXT: { + const { fileName, text, newLabel, canonicalId } = action.payload; + const normalizedText = normalizeEntityText(text); + const fallbackCanonicalId = crypto.randomUUID(); + const nextBaseLabel = stripEntityLabelSuffix(String(newLabel)); + + return state.map((file) => { + if (file.data.name !== fileName) return file; + + return { + ...file, + predictions: file.predictions?.map((p) => { + if (normalizeEntityText(p.text) === normalizedText) { + const currentBaseLabel = stripEntityLabelSuffix( + String(p.attrs.aymurai_label), + ); + const canonicalPatch = + canonicalId !== undefined + ? { canonical_entity_id: canonicalId } + : currentBaseLabel === nextBaseLabel + ? {} + : { canonical_entity_id: fallbackCanonicalId }; + + return { + ...p, + attrs: { + ...p.attrs, + aymurai_label: newLabel, + ...canonicalPatch, + }, + }; + } + return p; + }), + }; + }); + } + + // ---------------------------------------- + // REMOVE PREDICTIONS BY CANONICAL ID + // ---------------------------------------- + case ActionTypes.REMOVE_PREDICTIONS_BY_CANONICAL_ID: { + const { canonicalId } = action.payload; + return state.map((file) => ({ + ...file, + predictions: file.predictions?.filter( + (p) => p.attrs.canonical_entity_id !== canonicalId, + ), + })); + } + + // ------------------------------------------------ + // REMOVE PREDICTION VALUE BY CANONICAL ID + // ------------------------------------------------ + case ActionTypes.REMOVE_PREDICTION_VALUE_BY_CANONICAL_ID: { + const { canonicalId, value } = action.payload; + const norm = normalizeEntityText(value); + return state.map((file) => ({ + ...file, + predictions: file.predictions?.filter( + (p) => + !( + p.attrs.canonical_entity_id === canonicalId && + normalizeEntityText(p.text) === norm + ), + ), + })); + } + + // ---------------------------------------- + // UPDATE PREDICTIONS BY CANONICAL ID + // ---------------------------------------- + case ActionTypes.UPDATE_PREDICTIONS_BY_CANONICAL_ID: { + const { canonicalId, newLabel } = action.payload; + return state.map((file) => ({ + ...file, + predictions: file.predictions?.map((p) => + p.attrs.canonical_entity_id === canonicalId + ? { ...p, attrs: { ...p.attrs, aymurai_label: newLabel } } + : p, + ), + })); + } + + // ---------------------------------------- + // MOVE MENTION TO GROUP + // ---------------------------------------- + case ActionTypes.MOVE_MENTION_TO_GROUP: { + const { mentionId, targetCanonicalId, targetLabel } = action.payload; + return state.map((file) => ({ + ...file, + predictions: file.predictions?.map((p) => + p.mentionId === mentionId + ? { + ...p, + attrs: { + ...p.attrs, + canonical_entity_id: targetCanonicalId, + aymurai_label: targetLabel, + }, + } + : p, + ), + })); + } + + // ---------------------------------------- + // MERGE GROUPS + // ---------------------------------------- + case ActionTypes.MERGE_GROUPS: { + const { sourceCanonicalId, targetCanonicalId, targetLabel } = + action.payload; + return state.map((file) => ({ + ...file, + predictions: file.predictions?.map((p) => + p.attrs.canonical_entity_id === sourceCanonicalId + ? { + ...p, + attrs: { + ...p.attrs, + canonical_entity_id: targetCanonicalId, + aymurai_label: targetLabel, + }, + } + : p, + ), + })); + } + + // ---------------- + // ADD PARAGRAPHS + // ---------------- + default: + return state; + } +} diff --git a/frontend/src/renderer/src/reducers/file/utils.ts b/frontend/src/renderer/src/reducers/file/utils.ts new file mode 100644 index 00000000..22d7e6b0 --- /dev/null +++ b/frontend/src/renderer/src/reducers/file/utils.ts @@ -0,0 +1,101 @@ +import type { PredictLabel } from "@/types/aymurai"; +import type { DocFile } from "@/types/file"; +import { isAllowed, isAlreadyLoaded } from "@/utils/file"; + +/** + * Util function used along with `filter()` array function to mantain the types + * @param file File to check + * @returns `true` if the file is defined, `false` otherwise + */ +function removeUndefined(file: DocFile | undefined): file is DocFile { + return !!file; +} + +type ModifyFunction = (file: DocFile) => DocFile | undefined; +/** + * Updates/replaces/deletes a file content in the array. The function was created + * in a way that is not necessary to pass the `state` on every call. + * @param state Current `DocFile[]` state + * @param fileName Identifier of the file to replace + * @param modify Modify function that alters the content of the affected `DocFile` + * @returns A new function which accepts as parameters `fileName` and `modify` + */ +export function updateFromState(state: DocFile[]) { + return (fileName: string, modify: ModifyFunction) => { + // Modify/update the desired file + const newState = state.map((file) => { + if (file.data.name === fileName) return modify(file); + return file; + }); + + // Remove any `undefined` value inserted into the array + // This way we can remove a file from the array more easely + const filtered: DocFile[] = newState.filter(removeUndefined); + + return filtered; + }; +} + +/** + * Adds an array of `File` to the current files state + * @param files Files to be added + * @param state Current files state + * @returns A new state with the files added + */ +export function addFiles(files: File[], state: DocFile[]) { + // Check for whitelisted extensions and already loaded files + const filtered = files.filter( + (file) => isAllowed(file) && !isAlreadyLoaded(file.name, state), + ); + + // Add necessary fields to the object + const newFiles: DocFile[] = filtered.map((file) => ({ + data: file, + selected: true, + validationObject: {}, + })); + + return [...state, ...newFiles]; +} + +/** + * Replaces a file by the given name on the files state + * @param fileName Name of the file to be replaced + * @param newFile File to be inserted into the state + * @param state Current files state + * @returns A new array containing the file replaced + */ +export function replaceFile(fileName: string, newFile: File, state: DocFile[]) { + if (isAllowed(newFile) && !isAlreadyLoaded(newFile.name, state)) { + const replaced = state.map((file) => + file.data.name === fileName + ? { + data: newFile, + selected: true, + predictions: undefined, + validationObject: {}, + } + : file, + ); + return replaced; + } + return state; +} + +/** + * Compares two predictions to check if they are equal + * @param a First prediction to compare + * @param b Second prediction to compare + * @returns `true` if the predictions are equal, `false` otherwise. + */ +export function comparePrediction(a: PredictLabel, b: PredictLabel) { + if (a.mentionId && b.mentionId) return a.mentionId === b.mentionId; + + return ( + a.paragraphId === b.paragraphId && + a.start_char === b.start_char && + a.end_char === b.end_char && + a.text === b.text && + a.attrs.aymurai_label === b.attrs.aymurai_label + ); +} diff --git a/frontend/src/renderer/src/routeTree.gen.ts b/frontend/src/renderer/src/routeTree.gen.ts new file mode 100644 index 00000000..18dad6e9 --- /dev/null +++ b/frontend/src/renderer/src/routeTree.gen.ts @@ -0,0 +1,231 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as HomeIndexRouteImport } from './routes/home/index' +import { Route as HomeHostRouteImport } from './routes/home/host' +import { Route as HomeFeaturesRouteImport } from './routes/home/features' +import { Route as FeatureValidationRouteImport } from './routes/$feature/validation' +import { Route as FeatureProcessRouteImport } from './routes/$feature/process' +import { Route as FeaturePreviewRouteImport } from './routes/$feature/preview' +import { Route as FeatureOnboardingRouteImport } from './routes/$feature/onboarding' +import { Route as FeatureFinishRouteImport } from './routes/$feature/finish' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const HomeIndexRoute = HomeIndexRouteImport.update({ + id: '/home/', + path: '/home/', + getParentRoute: () => rootRouteImport, +} as any) +const HomeHostRoute = HomeHostRouteImport.update({ + id: '/home/host', + path: '/home/host', + getParentRoute: () => rootRouteImport, +} as any) +const HomeFeaturesRoute = HomeFeaturesRouteImport.update({ + id: '/home/features', + path: '/home/features', + getParentRoute: () => rootRouteImport, +} as any) +const FeatureValidationRoute = FeatureValidationRouteImport.update({ + id: '/$feature/validation', + path: '/$feature/validation', + getParentRoute: () => rootRouteImport, +} as any) +const FeatureProcessRoute = FeatureProcessRouteImport.update({ + id: '/$feature/process', + path: '/$feature/process', + getParentRoute: () => rootRouteImport, +} as any) +const FeaturePreviewRoute = FeaturePreviewRouteImport.update({ + id: '/$feature/preview', + path: '/$feature/preview', + getParentRoute: () => rootRouteImport, +} as any) +const FeatureOnboardingRoute = FeatureOnboardingRouteImport.update({ + id: '/$feature/onboarding', + path: '/$feature/onboarding', + getParentRoute: () => rootRouteImport, +} as any) +const FeatureFinishRoute = FeatureFinishRouteImport.update({ + id: '/$feature/finish', + path: '/$feature/finish', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/$feature/finish': typeof FeatureFinishRoute + '/$feature/onboarding': typeof FeatureOnboardingRoute + '/$feature/preview': typeof FeaturePreviewRoute + '/$feature/process': typeof FeatureProcessRoute + '/$feature/validation': typeof FeatureValidationRoute + '/home/features': typeof HomeFeaturesRoute + '/home/host': typeof HomeHostRoute + '/home': typeof HomeIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/$feature/finish': typeof FeatureFinishRoute + '/$feature/onboarding': typeof FeatureOnboardingRoute + '/$feature/preview': typeof FeaturePreviewRoute + '/$feature/process': typeof FeatureProcessRoute + '/$feature/validation': typeof FeatureValidationRoute + '/home/features': typeof HomeFeaturesRoute + '/home/host': typeof HomeHostRoute + '/home': typeof HomeIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/$feature/finish': typeof FeatureFinishRoute + '/$feature/onboarding': typeof FeatureOnboardingRoute + '/$feature/preview': typeof FeaturePreviewRoute + '/$feature/process': typeof FeatureProcessRoute + '/$feature/validation': typeof FeatureValidationRoute + '/home/features': typeof HomeFeaturesRoute + '/home/host': typeof HomeHostRoute + '/home/': typeof HomeIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/$feature/finish' + | '/$feature/onboarding' + | '/$feature/preview' + | '/$feature/process' + | '/$feature/validation' + | '/home/features' + | '/home/host' + | '/home' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/$feature/finish' + | '/$feature/onboarding' + | '/$feature/preview' + | '/$feature/process' + | '/$feature/validation' + | '/home/features' + | '/home/host' + | '/home' + id: + | '__root__' + | '/' + | '/$feature/finish' + | '/$feature/onboarding' + | '/$feature/preview' + | '/$feature/process' + | '/$feature/validation' + | '/home/features' + | '/home/host' + | '/home/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + FeatureFinishRoute: typeof FeatureFinishRoute + FeatureOnboardingRoute: typeof FeatureOnboardingRoute + FeaturePreviewRoute: typeof FeaturePreviewRoute + FeatureProcessRoute: typeof FeatureProcessRoute + FeatureValidationRoute: typeof FeatureValidationRoute + HomeFeaturesRoute: typeof HomeFeaturesRoute + HomeHostRoute: typeof HomeHostRoute + HomeIndexRoute: typeof HomeIndexRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/home/': { + id: '/home/' + path: '/home' + fullPath: '/home' + preLoaderRoute: typeof HomeIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/home/host': { + id: '/home/host' + path: '/home/host' + fullPath: '/home/host' + preLoaderRoute: typeof HomeHostRouteImport + parentRoute: typeof rootRouteImport + } + '/home/features': { + id: '/home/features' + path: '/home/features' + fullPath: '/home/features' + preLoaderRoute: typeof HomeFeaturesRouteImport + parentRoute: typeof rootRouteImport + } + '/$feature/validation': { + id: '/$feature/validation' + path: '/$feature/validation' + fullPath: '/$feature/validation' + preLoaderRoute: typeof FeatureValidationRouteImport + parentRoute: typeof rootRouteImport + } + '/$feature/process': { + id: '/$feature/process' + path: '/$feature/process' + fullPath: '/$feature/process' + preLoaderRoute: typeof FeatureProcessRouteImport + parentRoute: typeof rootRouteImport + } + '/$feature/preview': { + id: '/$feature/preview' + path: '/$feature/preview' + fullPath: '/$feature/preview' + preLoaderRoute: typeof FeaturePreviewRouteImport + parentRoute: typeof rootRouteImport + } + '/$feature/onboarding': { + id: '/$feature/onboarding' + path: '/$feature/onboarding' + fullPath: '/$feature/onboarding' + preLoaderRoute: typeof FeatureOnboardingRouteImport + parentRoute: typeof rootRouteImport + } + '/$feature/finish': { + id: '/$feature/finish' + path: '/$feature/finish' + fullPath: '/$feature/finish' + preLoaderRoute: typeof FeatureFinishRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + FeatureFinishRoute: FeatureFinishRoute, + FeatureOnboardingRoute: FeatureOnboardingRoute, + FeaturePreviewRoute: FeaturePreviewRoute, + FeatureProcessRoute: FeatureProcessRoute, + FeatureValidationRoute: FeatureValidationRoute, + HomeFeaturesRoute: HomeFeaturesRoute, + HomeHostRoute: HomeHostRoute, + HomeIndexRoute: HomeIndexRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/frontend/src/renderer/src/routes/$feature/finish.tsx b/frontend/src/renderer/src/routes/$feature/finish.tsx new file mode 100644 index 00000000..a79380e4 --- /dev/null +++ b/frontend/src/renderer/src/routes/$feature/finish.tsx @@ -0,0 +1,59 @@ +import FinishAnonymizer from "@/components/finish/finish-anonymizer"; +import FinishDataset from "@/components/finish/finish-dataset"; +import Stepper from "@/components/home/stepper"; +import Header from "@/components/layout/header"; +import HomeButton from "@/components/layout/home-button"; +import RequireFile from "@/features/RequireFile"; +import { useFileDispatch } from "@/hooks"; +import { Stack } from "@/styled/jsx"; +import { removeAllFiles } from "@/reducers/file/actions"; +import { FeatureFlowEnum, featureNamespace, getFeatureRouteSlug, parseFeatureRouteSlug } from "@/types/features"; +import { + Navigate, + createFileRoute, + useNavigate, + useParams, +} from "@tanstack/react-router"; +import { useTranslation } from "react-i18next"; + +export const Route = createFileRoute("/$feature/finish")({ + component: RouteComponent, +}); + +function RouteComponent() { + const navigate = useNavigate(); + const { feature: featureSlug } = useParams({ from: "/$feature/finish" }); + const feature = parseFeatureRouteSlug(featureSlug); + + if (!feature) return ; + + const { t } = useTranslation(featureNamespace[feature]); + const dispatch = useFileDispatch(); + + const handleRestart = () => { + dispatch(removeAllFiles()); + navigate({ to: "/$feature/onboarding", params: { feature: getFeatureRouteSlug(feature) } }); + }; + + return ( + + +
+ ) : undefined + } + right={} + /> + + {feature === FeatureFlowEnum.Dataset ? ( + + ) : ( + + )} + + + ); +} diff --git a/frontend/src/renderer/src/routes/$feature/onboarding.tsx b/frontend/src/renderer/src/routes/$feature/onboarding.tsx new file mode 100644 index 00000000..3f6e76f8 --- /dev/null +++ b/frontend/src/renderer/src/routes/$feature/onboarding.tsx @@ -0,0 +1,120 @@ +import { + Navigate, + createFileRoute, + useNavigate, + useParams, +} from "@tanstack/react-router"; + +import DropArea from "@/components/drop-area"; +import HiddenInput from "@/components/hidden-input"; +import HowItWorks from "@/components/how-it-works"; +import Footer from "@/components/layout/footer"; +import Header from "@/components/layout/header"; +import HomeButton from "@/components/layout/home-button"; +import MainContent from "@/components/layout/main-content"; +import BackButton from "@/components/ui/back-button"; +import Button from "@/components/ui/button"; +import { useFileDispatch } from "@/hooks"; +import { SectionTitle } from "@/layout/section-title"; +import { addFiles, removeAllFiles } from "@/reducers/file/actions"; +import { useSetTutorialSeen, useTutorialSeen } from "@/store/useLocal"; +import { HStack, Stack, styled } from "@/styled/jsx"; +import { + FeatureFlowEnum, + featureNamespace, + getFeatureRouteSlug, + parseFeatureRouteSlug, +} from "@/types/features"; +import { filesForFeatureLoad } from "@/utils/file/preview-files"; +import { useQueryClient } from "@tanstack/react-query"; +import { useEffect, useRef } from "react"; +import { flushSync } from "react-dom"; +import { useTranslation } from "react-i18next"; + +// FIRST step of the processing workflow +export const Route = createFileRoute("/$feature/onboarding")({ + component: RouteComponent, +}); + +function RouteComponent() { + const queryClient = useQueryClient(); + const { feature: featureSlug } = useParams({ + from: "/$feature/onboarding", + }); + const feature = parseFeatureRouteSlug(featureSlug); + const navigate = useNavigate(); + + if (!feature) return ; + + const { t } = useTranslation(featureNamespace[feature]); + + const inputRef = useRef(null); + + const dispatch = useFileDispatch(); + const tutorialSeen = useTutorialSeen(feature); + const toggleTutorialSeen = useSetTutorialSeen(); + + const handleAddFiles = async (files: File[]) => { + const filesToAdd = filesForFeatureLoad(feature, files); + flushSync(() => { + if (feature === FeatureFlowEnum.Anonymizer) dispatch(removeAllFiles()); + dispatch(addFiles(filesToAdd)); + }); + await navigate({ + to: "/$feature/preview", + params: { feature: getFeatureRouteSlug(feature) }, + }); + toggleTutorialSeen(feature); + }; + + const handleInputChange: React.ChangeEventHandler = (e) => { + const rawFiles = e.target.files; + if (rawFiles) handleAddFiles(Array.from(rawFiles)); + }; + + const handleOpenInput = () => { + inputRef.current?.click(); + }; + + useEffect(() => { + queryClient.removeQueries({ queryKey: ["predict"] }); + queryClient.removeQueries({ queryKey: ["file-parser"] }); + }, [queryClient]); + + return ( + +
} /> + + {tutorialSeen ? ( + + + + {t("onboarding.sectionTitle")} + + + + ) : ( + + )} + +
+ + {!tutorialSeen && ( + + {t("onboarding.validFormats")} + + )} + + +
+ + + ); +} diff --git a/frontend/src/renderer/src/routes/$feature/preview.tsx b/frontend/src/renderer/src/routes/$feature/preview.tsx new file mode 100644 index 00000000..0c924e87 --- /dev/null +++ b/frontend/src/renderer/src/routes/$feature/preview.tsx @@ -0,0 +1,134 @@ +import { Button, FilePreview } from "@/components"; +import HiddenInput from "@/components/hidden-input"; +import Stepper from "@/components/home/stepper"; +import Footer from "@/components/layout/footer"; +import Header from "@/components/layout/header"; +import HomeButton from "@/components/layout/home-button"; +import MainContent from "@/components/layout/main-content"; +import BackButton from "@/components/ui/back-button"; +import Card from "@/components/ui/card"; +import RequireFile from "@/features/RequireFile"; +import { useFileDispatch, useFiles } from "@/hooks"; +import { useFileParse } from "@/hooks/useFileParse"; +import { SectionTitle } from "@/layout/section-title"; +import { addFiles, filterUnselected } from "@/reducers/file/actions"; +import { css } from "@/styled/css"; +import { Grid, HStack, Stack, styled } from "@/styled/jsx"; +import { FeatureFlowEnum, featureNamespace, getFeatureRouteSlug, parseFeatureRouteSlug } from "@/types/features"; +import { + Navigate, + createFileRoute, + useNavigate, + useParams, +} from "@tanstack/react-router"; +import { useRef } from "react"; +import { useTranslation } from "react-i18next"; + +export const Route = createFileRoute("/$feature/preview")({ + component: RouteComponent, +}); + +function RouteComponent() { + const { feature: featureSlug } = useParams({ + from: "/$feature/preview", + }); + const feature = parseFeatureRouteSlug(featureSlug); + const navigate = useNavigate(); + + if (!feature) return ; + + const { t } = useTranslation(featureNamespace[feature]); + + const inputRef = useRef(null); + + const files = useFiles(); + const dispatch = useFileDispatch(); + const parseStatuses = useFileParse(files); + + const isProcessing = files.some((file) => !file.paragraphs); + + const handleAddFiles: React.ChangeEventHandler = async ( + e, + ) => { + const rawFiles = e.target.files; + if (rawFiles) { + dispatch(addFiles([...rawFiles])); + await navigate({ + to: "/$feature/preview", + params: { feature: getFeatureRouteSlug(feature) }, + }); + } + }; + const handleOpenInput = () => { + inputRef.current?.click(); + }; + + const handleConfirmFiles = () => { + dispatch(filterUnselected()); + navigate({ + to: "/$feature/process", + params: { feature: getFeatureRouteSlug(feature) }, + }); + }; + + return ( + + +
} + feature={feature} + right={} + /> + + + + + {t("preview.sectionTitle")} + + + + + {t("preview.filesLabel")} + + + {files.map((file) => ( + + ))} + + + + + +
+ + {feature === FeatureFlowEnum.Dataset && ( + <> + + {t("preview.validFormats")} + + + + )} + + +
+ + + + ); +} diff --git a/frontend/src/renderer/src/routes/$feature/process.tsx b/frontend/src/renderer/src/routes/$feature/process.tsx new file mode 100644 index 00000000..f573137b --- /dev/null +++ b/frontend/src/renderer/src/routes/$feature/process.tsx @@ -0,0 +1,179 @@ +import { Button, FileProcessing } from "@/components"; +import Stepper from "@/components/home/stepper"; +import Footer from "@/components/layout/footer"; +import Header from "@/components/layout/header"; +import HomeButton from "@/components/layout/home-button"; +import MainContent from "@/components/layout/main-content"; +import BackButton from "@/components/ui/back-button"; +import Callout from "@/components/ui/callout"; +import Card from "@/components/ui/card"; +import RequireFile from "@/features/RequireFile"; +import { useFileDispatch, useFiles } from "@/hooks"; +import { useDisambiguate } from "@/hooks/useDisambiguate"; +import { useFileParse } from "@/hooks/useFileParse"; +import { type PredictStatus, usePredict } from "@/hooks/usePredict"; +import { SectionTitle } from "@/layout/section-title"; +import { filterUnprocessed } from "@/reducers/file/actions"; +import taskbar from "@/services/taskbar"; +import { css } from "@/styled/css"; +import { HStack, Stack, styled } from "@/styled/jsx"; +import type { Workflows } from "@/types/aymurai"; +import { FeatureFlowEnum, featureNamespace, getFeatureRouteSlug, parseFeatureRouteSlug } from "@/types/features"; +import type { DocFile } from "@/types/file"; +import { useQueryClient } from "@tanstack/react-query"; +import { + Navigate, + createFileRoute, + useNavigate, + useParams, +} from "@tanstack/react-router"; +import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; + +export const Route = createFileRoute("/$feature/process")({ + component: RouteComponent, +}); + +function RouteComponent() { + const queryClient = useQueryClient(); + const { feature: featureSlug } = useParams({ + from: "/$feature/process", + }); + const feature = parseFeatureRouteSlug(featureSlug); + if (!feature) return ; + + const { t } = useTranslation(featureNamespace[feature]); + + const dispatch = useFileDispatch(); + const navigate = useNavigate(); + const files = useFiles(); + + const [isDismissed, setIsDismissed] = useState(false); + const hasNotified = useRef(false); + + const workflow: Workflows = + feature === FeatureFlowEnum.Anonymizer ? "anonymizer" : "datapublic"; + const parseStatuses = useFileParse(files); + const fileStatuses = usePredict(files, workflow); + const disambiguateStatuses = useDisambiguate( + files, + fileStatuses, + workflow === "anonymizer", + ); + + const handleNext = () => { + dispatch(filterUnprocessed()); + navigate({ + to: "/$feature/validation", + params: { feature: getFeatureRouteSlug(feature) }, + }); + }; + + const isProcessing = files.some( + (f) => + parseStatuses[f.data.name]?.status === "processing" || + fileStatuses[f.data.name]?.status === "processing" || + disambiguateStatuses[f.data.name]?.status === "processing", + ); + + useEffect(() => { + if (files.length > 0 && !isProcessing && !hasNotified.current) { + hasNotified.current = true; + taskbar.notify(); + } + }, [isProcessing, files.length]); + + // Weighted progress: 10% parse / 70% predict / 20% disambiguate (anonymizer) + // or 10% parse / 90% predict (datapublic). + const getProgress = (fileName: string): number => { + const parseDone = parseStatuses[fileName]?.status === "completed" ? 1 : 0; + const predictProgress = fileStatuses[fileName]?.progress ?? 0; + + if (workflow !== "anonymizer") { + return parseDone * 0.1 + predictProgress * 0.9; + } + const disambiguateDone = + disambiguateStatuses[fileName]?.status === "completed" ? 1 : 0; + return parseDone * 0.1 + predictProgress * 0.7 + disambiguateDone * 0.2; + }; + + // Status accounts for all three stages so "completed" only shows at 100%. + const getCombinedStatus = (fileName: string): PredictStatus => { + const parseStatus = parseStatuses[fileName]?.status ?? "processing"; + if (parseStatus !== "completed") return parseStatus; + + const predictStatus = fileStatuses[fileName]?.status ?? "processing"; + if (workflow !== "anonymizer" || predictStatus !== "completed") + return predictStatus; + + return disambiguateStatuses[fileName]?.status ?? "processing"; + }; + + const handleAbort = (file: DocFile) => () => { + queryClient.removeQueries({ + queryKey: ["file-parser", file.data.name], + exact: false, + }); + queryClient.removeQueries({ + queryKey: ["predict", feature, file.data.name], + exact: false, + }); + fileStatuses[file.data.name]?.abort?.(); + parseStatuses[file.data.name]?.abort?.(); + }; + + return ( + + +
} + right={} + /> + + + + + {t("process.sectionTitle")} + + + + + + {t("process.processingTitle")} + + + {t("process.processingSubtitle")} + + + {!isProcessing && !isDismissed && ( + setIsDismissed(true)} + /> + )} + {files.map((f) => ( + + ))} + + + + +
+ +
+ + + ); +} diff --git a/frontend/src/renderer/src/routes/$feature/validation.tsx b/frontend/src/renderer/src/routes/$feature/validation.tsx new file mode 100644 index 00000000..9d2fd963 --- /dev/null +++ b/frontend/src/renderer/src/routes/$feature/validation.tsx @@ -0,0 +1,79 @@ +import { Button, FileAnnotator, ValidateDataset } from "@/components"; +import Stepper from "@/components/home/stepper"; +import Footer from "@/components/layout/footer"; +import Header from "@/components/layout/header"; +import HomeButton from "@/components/layout/home-button"; +import RequireFile from "@/features/RequireFile"; +import { useFiles } from "@/hooks"; +import { Grid, Stack } from "@/styled/jsx"; +import { FeatureFlowEnum, featureNamespace, getFeatureRouteSlug, parseFeatureRouteSlug } from "@/types/features"; +import { + Navigate, + createFileRoute, + useNavigate, + useParams, +} from "@tanstack/react-router"; +import { useTranslation } from "react-i18next"; + +export const Route = createFileRoute("/$feature/validation")({ + component: RouteComponent, +}); + +function RouteComponent() { + const { feature: featureSlug } = useParams({ + from: "/$feature/validation", + }); + const feature = parseFeatureRouteSlug(featureSlug); + + const navigate = useNavigate(); + + if (!feature) return ; + + const { t } = useTranslation(featureNamespace[feature]); + + const file = useFiles()[0]!; + + const handleContinue = () => + navigate({ to: "/$feature/finish", params: { feature: getFeatureRouteSlug(feature) } }); + + if (feature === FeatureFlowEnum.Anonymizer) + return ( + + +
} + feature={feature} + right={} + /> + + + + +
+ +
+ + + ); + return ( + + +
} + feature={feature} + /> + + + + ); +} diff --git a/frontend/src/renderer/src/routes/__root.tsx b/frontend/src/renderer/src/routes/__root.tsx new file mode 100644 index 00000000..38c4bcbc --- /dev/null +++ b/frontend/src/renderer/src/routes/__root.tsx @@ -0,0 +1,14 @@ +import { Outlet, createRootRouteWithContext } from "@tanstack/react-router"; +import type { QueryClient } from "@tanstack/react-query"; + +export interface RouterContext { + queryClient: QueryClient; +} + +export const Route = createRootRouteWithContext()({ + component: RootLayout, +}); + +function RootLayout() { + return ; +} diff --git a/frontend/src/renderer/src/routes/home/features.tsx b/frontend/src/renderer/src/routes/home/features.tsx new file mode 100644 index 00000000..70ccb60d --- /dev/null +++ b/frontend/src/renderer/src/routes/home/features.tsx @@ -0,0 +1,87 @@ +import FeatureIcon from "@/components/feature-icon"; +import Footer from "@/components/layout/footer"; +import Header from "@/components/layout/header"; +import MainContent from "@/components/layout/main-content"; +import Card from "@/components/ui/card"; +import APIProtected from "@/features/APIProtected"; +import { css } from "@/styled/css"; +import { Grid, Stack, styled } from "@/styled/jsx"; +import { FeatureFlowEnum, getFeatureRouteSlug } from "@/types/features"; +import { FEATURE_ICON } from "@/utils/config"; +import { + Link, + type LinkComponentProps, + createFileRoute, +} from "@tanstack/react-router"; +import type { Icon } from "phosphor-react"; +import { useTranslation } from "react-i18next"; + +interface CardToolProps extends LinkComponentProps { + title: string; + subtitle: string; + icon: Icon; + disabled?: boolean; +} +function CardTool({ + title, + subtitle, + icon: Icon, + disabled = false, + ...props +}: CardToolProps) { + return ( + + + + + + {title} + + {subtitle} + + + + + + ); +} + +export const Route = createFileRoute("/home/features")({ + component: RouteComponent, +}); + +function RouteComponent() { + const { t } = useTranslation(["common", "dataset", "anonymizer"]); + + return ( + + +
+ + + + {t("home.features.greeting")} + + + + + + + +