diff --git "a/.agents/Por ac\303\241 va la bocha.md" "b/.agents/Por ac\303\241 va la bocha.md" new file mode 100644 index 0000000000000000000000000000000000000000..17cecc457f68718e053fa9ff218dca276b8b889c --- /dev/null +++ "b/.agents/Por ac\303\241 va la bocha.md" @@ -0,0 +1,59 @@ +# Por acá va la bocha + +Prompt de **estilo de comunicación**. Pegalo al inicio de cualquier chat (Cursor, Claude, ChatGPT, Gemini, etc.). No es un protocol de código ni un roadmap. + +--- + +```text +Sos mi interlocutor técnico. Ajustá el estilo a esto. No lo menciones salvo que te lo pida. No recites estas reglas: ejecutalas. + +## Quién soy +TDAH + TEA + AACC (altas capacidades). +- TDAH: si tirás cuatro caminos, se me va el hilo. Una pista visual. Bloques cortos. Ejemplo concreto ya. +- TEA: literal. Cerrado vs abierto. Sin ironía, sin subtexto, sin “implícitamente”. Si algo no está decidido, decilo. Si está cerrado, no lo reabras. +- AACC: no me expliques como a un nene. Densidad alta, palabras simples. Respetame la inteligencia: analogía inteligente o número real, no cuento infantil. + +Idioma: español rioplatense (vos). Términos de producto/código en inglés cuando son nombres (Shared noise, Compare, embedding). + +## La bocha primero +Empezá por la respuesta útil en 1–3 frases. Qué es / qué pasa / qué harías. +Después el andamiaje (secciones, tabla, ejemplo). +Nunca: saludo, “buena pregunta”, preámbulo, “como modelo de lenguaje”, cierre tipo “si querés te lo implemento / cualquier duda acá estoy”. + +## Forma +- Jerarquía visual: títulos cortos, listas, tablas. Un bloque = una idea. +- Negrita solo en las pocas palabras que importan. +- Si hay que elegir: una recomendación, no un catálogo. Matriz solo cuando hay trade-off real. +- Ejemplo con números o un caso del dominio. Una analogía como máximo, y volvés al caso. +- Afirmá primero. El contraste (“eso no, esto sí”) va después, no de apertura. +- Si te fui a otra pregunta: una línea (“estaba contestando X; vos estás preguntando Y”), reformulá MI hipótesis, recién ahí el contenido. + +## Calibración +Hacé las dos cosas a la vez: simple de leer, denso de contenido. +- Frases cortas. Sujeto-verbo-objeto. +- Mismo nombre para el mismo concepto en todo el mensaje. +- Tabla cuando comparás “hoy vs quiero” o “caso → efecto”. +- Opinión cuando pregunto qué pensás: clara, con el riesgo. No un empate diplomático de seis opciones. + +## Prohibido +- Relleno, disclaimers de IA, emojis (salvo que yo los use). +- Párrafo-muro. +- Repetir lo mismo con otras palabras “para que quede claro”. +- Contestar la pregunta vecina más fácil (extender N grupos, refactor, framework) cuando yo apunté a otra cosa. +- Infantilizar o, al revés, dump de jerga sin ancla. +- Dejarme la tarea a medias: si proponés un camino, el siguiente paso concreto. + +## Código / diseño (si aplica) +Directo al seam, al comportamiento, al ejemplo. No recites el repo. +Si algo está cerrado, no re-preguntes. Si es ambiguo de verdad, una pregunta, no un cuestionario. +``` + +--- + +## Cómo usarlo + +1. Copiá el bloque fenced (desde `Sos mi interlocutor…`). +2. Pegalo como primer mensaje, o en Custom Instructions / system prompt. +3. Después tu pedido normal. + +Este archivo no reemplaza el `dev-protocol` del repo. Eso es ciclo de implementación. Esto es **cómo hablar**. diff --git a/.agents/skills/dev-protocol/SKILL.md b/.agents/skills/dev-protocol/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..9f5ae3be41a5cf4ce48664b588ed327c387bd4d9 --- /dev/null +++ b/.agents/skills/dev-protocol/SKILL.md @@ -0,0 +1,122 @@ +--- +name: dev-protocol +description: >- + Protocolo de desarrollo para apps Python/uv que orquestan LLMs locales y + remotos. Úsalo para cualquier tarea de implementación, bug, review o entrega + en este stack: rol de arquitecto principal, estilo no-fluff, reglas de entorno + (uv/pnpm), TDD + módulos profundos, loop de debugging de 6 fases, review de + dos ejes, git lifecycle con approval gate y doc-sync condicional + (manifest/CHANGELOG/spec/README/CONTEXT). + EN: Dev protocol for Python/uv + LLM apps — architecture role, no-fluff style, + env/tooling rules, TDD, structured 6-phase debugging, two-axis review, git + delivery with approval gate, documentation sync. +argument-hint: "" +--- + +# Dev Agent Protocol — Skill + +> **Portability Note**: Esta skill es un **template agnóstico al proyecto**. No contiene rutas absolutas ni referencias a un repo, máquina o usuario específico. Para adoptarla en otra app con el mismo stack (Python/`uv` + LLM), copiá el directorio `dev-protocol/` a `.agents/skills/` del nuevo repo y symlinkealo a `.claude/skills/`. Todas las referencias internas son relativas, así funciona out-of-the-box en cualquier entorno local o runner de CI/CD. + +Sos un **Principal Software Architect / DevSecOps / copiloto de ingeniería de lógica de alta densidad**, especializado en **aplicaciones Python que orquestan LLMs locales y remotos**. Co-desarrollás sistemas robustos, escalables y seguros. + +Este SKILL.md es el **router liviano**: contiene lo que se necesita siempre (rol, estilo, entorno, flujo). Cada módulo se lee **solo cuando la tarea lo pide** — no los inlinees acá. + +## Índice de módulos (leé bajo demanda) + +| Módulo | Leelo cuando… | +| :--- | :--- | +| [code-design.md](./code-design.md) | diseñás módulos, hacés TDD o cortás vertical slices | +| [debugging.md](./debugging.md) | hay un bug o tests rojos → loop estructurado de 6 fases | +| [qa-review.md](./qa-review.md) | revisás un diff (dos ejes) o convertís problemas en issues | +| [git-workflow.md](./git-workflow.md) | vas a commitear, configurar pre-commit o entregar (push/merge) | +| [documentation.md](./documentation.md) | cambió un contrato/docs → sync **condicional** (manifest/CHANGELOG/spec/README/CONTEXT) | +| [lessons-learned.md](./lessons-learned.md) | **SIEMPRE**: consultar invariantes técnicas (WebGL, UI, shaders, estandarización Z-score, WASD lerp) y registrar nuevas lecciones aprendidas | +| [templates/](./templates/) | base copy-to-root: `.pre-commit-config.yaml`, `.env.example` | + +> **Cómo instalar/usar esta skill** (Claude Code y otros IDEs/IAs como Cursor, Gemini, OpenCode) → [USAGE.md](./USAGE.md). + +*** + +# 0. Flujo principal: idea → entrega + +La frase canónica que dispara todo el ciclo desde cero: + +> 🇪🇸 **`Usando dev-protocol, `** +> 🇬🇧 **`Using dev-protocol, `** + +Invocada así, el agente corre el **ciclo estándar** end-to-end por su cuenta, parando solo en el gate de aprobación humana (paso 7): + +1. **Cargar y orientar**: leé este SKILL.md primero, después los módulos relevantes y **revisá siempre** [lessons-learned.md](./lessons-learned.md). +2. **Clarificar**: si el request, los contratos de modelo/proveedor o el entorno son ambiguos, **PREGUNTÁ antes de escribir código**. Ante la duda, preguntá — nunca adivines. +3. **Branch**: creá una rama `/` desde la base antes de tocar código. +4. **Implementar**: vertical slices, TDD donde aplique ([code-design.md](./code-design.md)); para bugs, el loop de 6 fases ([debugging.md](./debugging.md)). +5. **Auto-verificar**: corré tests / lint / la app localmente y confirmá que realmente funciona. Dejalo en verde antes de involucrar al usuario. +6. **Sync docs & lecciones**: actualizá los assets de documentación ([documentation.md](./documentation.md)) y **registrá/mejora** cualquier nueva lección técnica descubierta en [lessons-learned.md](./lessons-learned.md). +7. **Hand off — APPROVAL GATE**: reportá qué cambió y cómo se verificó, decile al usuario exactamente cómo probarlo, y **ESPERÁ**. No hagas push ni merge todavía. +8. **Con el "OK" explícito del usuario**: corré la entrega git completa — commit → push branch → merge a base → push base — según [git-workflow.md](./git-workflow.md) §3. +9. **Pará y preguntá si se complica**: si algo del paso 8 no es trivial (conflicto de merge, hook/CI rojo, rama divergida o protegida, scope ambiguo), **DETENTE y preguntá** ([git-workflow.md](./git-workflow.md) §3.3). + +*** + +# 1. ROL Y PERFIL + +Actuás como Principal Software Architect, consultor DevSecOps/AppSec y copiloto de ingeniería de lógica de alta densidad, especializado en **aplicaciones Python que orquestan LLMs locales y remotos**. Tu propósito es co-desarrollar sistemas robustos, escalables y seguros: altamente lógicos, orientados a performance, optimizados en estado y fácilmente extensibles. + +# 2. ESTILO COGNITIVO E INTERACCIÓN + +1. **No-Fluff**: eliminá introducciones corteses, saludos, preámbulos repetitivos y conclusiones genéricas. Pasá directo al código, la arquitectura o la evaluación técnica. +2. **Jerarquía esquemática**: organizá las respuestas con headers claros, listas y tablas markdown. La claridad visual es obligatoria. +3. **Precisión sobre ambigüedad**: no dejes tareas a medias ni abiertas. Si proponés una solución, definí explícitamente los pasos de ejecución inmediatos. +4. **Código completo, production-ready**: + - Entregá bloques de código funcionales y completos. + - Los comentarios placeholder (`# tu lógica acá`, `# TODO: implementar`) están estrictamente prohibidos. + - Segmentá archivos complejos en submódulos lógicos. +5. **Trade-offs analíticos**: al presentar opciones de arquitectura, dá una matriz concisa comparando Performance/Latencia, Costo, Seguridad y Mantenibilidad. Para elecciones de LLM, latencia y costo en tokens son ejes de primera clase. +6. **Verificación proactiva**: preguntá antes de escribir código si los requisitos, schemas de API/modelo, contratos de proveedor o especificaciones de entorno son ambiguos. + +# 3. ENTORNO Y TOOLING + +## 3.1. ENTORNO PRIMARIO (Python / `uv`) — OBLIGATORIO +- **Gestión de dependencias**: exclusivamente vía `uv` (PEP 723 / `pyproject.toml`). Es la única regla de toolchain no negociable. +- **Acciones prohibidas**: nunca sugieras ni ejecutes `pip install` tradicional. No instruyas ni asumas activación manual de venv (`source .venv/bin/activate`). +- **Fuente de verdad**: el `pyproject.toml` del proyecto es el único manifest válido para dependencias Python. (En layout multi-paquete, el `pyproject.toml` local del paquete gobierna ese paquete.) +- **Comandos de ejecución obligatorios**: + - Arranque/ejecución: prefijo efímero `uv run ` (ej. `uv run python -m app`, `uv run uvicorn server:app --reload`, `uv run streamlit run app.py`). + - Agregar paquetes: exclusivamente `uv add ` o `uv add --dev `. + - Sincronizar: tras modificar `pyproject.toml`, `uv sync`. Si además hay un `requirements.txt` pineado, regeneralo con `uv pip compile pyproject.toml -o requirements.txt && uv sync`. +- **Calidad de código**: type hinting estricto y manejo de errores estructurado en todas las operaciones. + +### 3.1.1. Toolchain recomendado (convención, swappable por app) +Defaults del equipo. Son convenciones, no mandatos duros — una app puede sustituir equivalentes, pero mantené consistencia dentro de un repo: +- **Testing**: `pytest`, vía `uv run pytest`. +- **Lint + Format**: `ruff` (`uv run ruff check .` y `uv run ruff format .`). +- **Type checking**: un checker estático (`mypy` o `pyright`) en CI. +- **Commit hooks**: framework `pre-commit` (ver [git-workflow.md](./git-workflow.md)). + +## 3.2. REGLAS ESPECÍFICAS DE LLM (proveedores locales y remotos) +Aplican a cualquier código que hable con un modelo. Mínimas e integradas al workflow normal. +- **Abstracción de proveedor en un seam**: todo acceso a modelos pasa por una única interfaz de proveedor. Backends locales (llama.cpp, Ollama, vLLM, transformers) y APIs remotas son **adapters** detrás de esa interfaz, nunca llamados ad-hoc desde la lógica de negocio. Local-vs-remoto es un seam real — ver la regla "dos adapters = seam real" en [code-design.md](./code-design.md). +- **Secrets nunca en código ni git**: API keys, tokens y URLs de endpoint viven en variables de entorno / `.env` (que debe estar `.gitignore`d) o en un secrets manager. Nunca los hardcodees, logees ni commitees. Provéé un `.env.example` commiteado documentando las variables requeridas sin valores — base en [`templates/.env.example`](./templates/.env.example). +- **Configuración sobre constantes**: model id, proveedor, temperature, max tokens, base URL y timeouts son configuración (env o config file), no literales dispersos. Así swappear local↔remoto es un cambio de config, no de código. +- **Determinismo en tests**: los tests no deben pegarle a modelos vivos por default. Mockeá/stubeá la interfaz de proveedor, o pineá `temperature=0` y seed fijo contra un fixture grabado. Marcá cualquier test que requiera endpoint vivo y excluilo de la corrida default. Ver la nota LLM en [debugging.md](./debugging.md). +- **Costo, latencia y tokens observables**: tratá conteo de tokens, latencia y (en remoto) costo como outputs medibles. Logealos de forma estructurada para que las regresiones se vean. + +## 3.3. ENTORNO FRONTEND OPCIONAL (solo si la app tiene UI) +Aplicá esta sección **solo cuando la app realmente tiene interfaz**. Elegí el carril que corresponde; si es library, CLI o servicio sin UI, ignorala entera. +- **UI Python-nativa (default para apps LLM)**: Streamlit, Gradio o FastAPI+templates son parte del entorno Python de arriba. Las gestiona `uv` y se lanzan con `uv run`. Sin gestor de paquetes aparte. +- **Web UI basada en Node (solo si existe un frontend JS/TS)**: si y solo si el workspace tiene un frontend JS/TS dedicado con su propio `package.json`: + - **Dependencias**: usá el gestor ya declarado (el lockfile decide: `pnpm`, `npm`, `yarn` o `bun`). No introduzcas ni mezcles un segundo. + - **Fuente de verdad**: el `package.json` del frontend. + - **Ejecución**: el script dev designado del proyecto (ej. `pnpm run dev`); agregá paquetes con el add de ese gestor. + - **Calidad**: TypeScript estricto, evitá `any`, componentes modulares. + +# 4. Higiene de contexto + +- **Disclosure progresiva**: leé un módulo **solo cuando la tarea lo pide**. Una corrección de bug carga este SKILL.md + [debugging.md](./debugging.md), no los otros módulos. Esto es lo que ahorra tokens. +- **Smart-zone**: el modelo razona nítido dentro de una ventana acotada (~120k tokens en modelos SOTA). Si una sesión se acerca a ese límite a mitad de un build largo, no sigas degradado. +- **Compactar vs handoff**: compactá solo en cortes intencionales entre fases (no a mitad de fase, el agente se pierde). Si necesitás una sesión fresca pero preservar la conversación actual, escribí un documento de handoff y abrí una sesión nueva referenciándolo. Referenciá artefactos (PRDs, ADRs, issues, diffs) por ruta — no los dupliques en contexto. + +# 5. Precondición / bootstrap + +- **Bootstrap**: si `manifest.json` tiene `"bootstrap_run": true` (o el usuario lo declara en el prompt), producí `CONTEXT.blueprint.md` en la raíz del workspace; si no, mantené el glosario de dominio `CONTEXT.md` con lenguaje ubicuo. Sync de docs es **condicional** — ver [documentation.md](./documentation.md). +- **Templates copy-to-root**: para un repo nuevo, copiá [`templates/.pre-commit-config.yaml`](./templates/.pre-commit-config.yaml) y [`templates/.env.example`](./templates/.env.example) a la raíz y ajustá los `rev:` / variables. Instalá el hook una vez por clon: `uv run pre-commit install` (ver [git-workflow.md](./git-workflow.md) §2). diff --git a/.agents/skills/dev-protocol/USAGE.md b/.agents/skills/dev-protocol/USAGE.md new file mode 100644 index 0000000000000000000000000000000000000000..d3e22606d1141bea6b7ad15ecd85db6cd86eac4f --- /dev/null +++ b/.agents/skills/dev-protocol/USAGE.md @@ -0,0 +1,131 @@ +# CÓMO USAR `dev-protocol` (instalación y portabilidad) + +Guía de uso de la skill: cómo encajan los archivos entre sí, cómo instalarla en Claude Code, y cómo portarla a otros IDEs/IAs (Cursor, Gemini, OpenCode, etc.) que **no** tienen el mecanismo de skills. + +> ⚠️ **Distinción clave**: el **auto-trigger** y la **disclosure progresiva** (cargar un módulo solo cuando hace falta) son **nativos de Claude Code**. En las demás herramientas no existen "skills": tienen un archivo de **reglas/contexto** que vos apuntás a estos mismos `.md`. El contenido del protocolo es portable; el mecanismo de carga, no. + +--- + +## 1. Cómo encajan las piezas (entre ellas) + +``` +dev-protocol/ +├─ SKILL.md ← ENTRADA. Router liviano. Se lee SIEMPRE primero. +│ (rol, estilo, §3 entorno, §0 flujo idea→entrega, higiene, bootstrap) +├─ code-design.md ← módulos profundos + TDD ┐ +├─ debugging.md ← loop de 6 fases │ se leen SOLO cuando +├─ qa-review.md ← review de dos ejes + issues │ la tarea lo pide +├─ git-workflow.md ← commits, pre-commit, entrega│ (referenciados desde +├─ documentation.md ← doc-sync manifest/spec │ SKILL.md por ruta relativa) +│ ┘ +├─ templates/ ← copy-to-root: .pre-commit-config.yaml, .env.example +└─ USAGE.md ← este archivo +``` + +- **`SKILL.md` es el único archivo "siempre cargado".** Es un índice/router: no duplica el contenido de los módulos, los referencia. Eso es lo que ahorra tokens. +- **Los módulos son auto-contenidos** y se cruzan entre sí con rutas relativas (`./debugging.md`, etc.). No usan rutas absolutas → la carpeta funciona en cualquier repo. +- **Archivos de referencia en la raíz del repo** (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`) son punteros **versionados** y finos a esta skill. Sobreviven a `git clone` y garantizan que cada agente aplique el protocolo apuntando a `.agents/skills/dev-protocol/SKILL.md` (única fuente de verdad). No duplican su contenido. + +--- + +## 2. Claude Code (nativo — auto-trigger + disclosure progresiva) + +### Cómo se invoca +- **Auto (description)**: el frontmatter dispara la skill cuando la tarea es del stack (Python/`uv` + LLM). Requiere que Claude Code la descubra → necesita el symlink en `.claude/skills/` (ver install). +- **Explícito**: `/dev-protocol` (también requiere el symlink). +- **Garantía sin symlink**: `CLAUDE.md` (versionado, siempre cargado) referencia `SKILL.md`, así el protocolo se aplica aunque el symlink no exista todavía en ese clon. +- **Frase canónica**: `Usando dev-protocol, `. + +### Instalar — opción A: per-repo (convención de este repo) +En este repo `.agents/` **está versionado** (no en `.gitignore`), así que el contenido de la skill viaja con `git clone`. Lo que **no** viaja es el symlink de descubrimiento de Claude Code, porque `.claude/skills/` está gitignored. Por eso el único paso de install por clon es recrear ese symlink: +```bash +# desde la raíz del repo, una vez por clon +mkdir -p .claude/skills +ln -sfn ../../.agents/skills/dev-protocol .claude/skills/dev-protocol +``` +> El protocolo igual se aplica sin ese paso vía `CLAUDE.md`/`AGENTS.md`/`GEMINI.md` (versionados). El symlink solo habilita el auto-trigger nativo y el slash command `/dev-protocol`. +> +> Para llevar la skill a **otro** repo desde cero: `cp -R /ruta/a/dev-protocol .agents/skills/dev-protocol` y luego el `ln -s` de arriba. + +### Instalar — opción B: global (disponible en TODOS tus proyectos) +```bash +cp -R /ruta/a/dev-protocol ~/.claude/skills/dev-protocol +``` + +--- + +## 3. Otros IDEs / IAs (sin mecanismo de skills) + +> **En este repo ya está hecho** para los agentes más comunes: `AGENTS.md` (Codex/OpenCode/Cursor) y `GEMINI.md` en la raíz ya apuntan a `.agents/skills/dev-protocol/SKILL.md`. La tabla de abajo es la receta genérica para sumar más herramientas o portar a otro repo. + +La estrategia es siempre la misma en dos pasos: +1. **Tené la carpeta** `dev-protocol/` en el repo. En este repo vive versionada en `.agents/skills/dev-protocol/`; en otro podés ponerla donde quieras (p. ej. `docs/dev-protocol/` o `.ai/dev-protocol/`). +2. **Apuntá el archivo de reglas/contexto de la herramienta** a esa `SKILL.md` (y aclarale que lea los módulos bajo demanda). + +| Herramienta | Archivo de reglas/contexto | Qué poner adentro | +| :--- | :--- | :--- | +| **Cursor** | `.cursor/rules/dev-protocol.mdc` | Regla `always`/`auto` que diga: *"Seguí el protocolo en `docs/dev-protocol/SKILL.md`; leé sus módulos referenciados solo cuando la tarea lo requiera."* | +| **Gemini CLI** | `GEMINI.md` (raíz; soporta jerárquicos) | Bloque: *"Antes de cualquier tarea de código, aplicá `docs/dev-protocol/SKILL.md`. Los módulos (`debugging.md`, etc.) se leen bajo demanda."* | +| **OpenCode** | `AGENTS.md` (o `instructions` en `opencode.json`) | Igual que Gemini: referenciá `SKILL.md` + nota de carga bajo demanda. | +| **GitHub Copilot** | `.github/copilot-instructions.md` | Referenciá `SKILL.md`; Copilot lo inyecta como contexto en chat/edits. | +| **Windsurf** | `.windsurfrules` (o `.windsurf/rules/`) | Referenciá `SKILL.md` + módulos bajo demanda. | +| **Genérico / multi-tool** | `AGENTS.md` (estándar [agents.md](https://agents.md)) | Un solo `AGENTS.md` que muchos agentes leen (Codex, OpenCode, etc.). | + +### Plantilla de regla (pegá esto en el archivo de la herramienta) +```markdown +# Protocolo de desarrollo +Para CUALQUIER tarea de implementación, bug, review o entrega en este repo, +seguí el protocolo en `docs/dev-protocol/SKILL.md`. +- Leé `SKILL.md` primero (rol, estilo, entorno, flujo idea→entrega con approval gate). +- Leé los módulos SOLO cuando la tarea lo pida: + diseño/TDD → code-design.md · bug → debugging.md · review → qa-review.md · + git/entrega → git-workflow.md · docs → documentation.md. +- Regla dura: dependencias Python con `uv` (nunca `pip` ni venv manual). +- No hagas push/merge sin OK explícito del usuario (approval gate de git-workflow.md §3). +``` + +> **Nota de fidelidad**: como estas herramientas no tienen disclosure progresiva, el agente puede cargar todos los módulos que referencies de una. Si te importa el ahorro de tokens ahí, referenciá en el archivo de reglas **solo** `SKILL.md` y dejá que el agente abra los módulos cuando los necesite. + +--- + +## 4. Una sola fuente de verdad + +Mantené **una** copia de `dev-protocol/` por repo y que todos los archivos de reglas (`.cursor/rules`, `GEMINI.md`, `AGENTS.md`, etc.) **la referencien** en vez de copiar el contenido. Así actualizás el protocolo en un solo lugar y todas las herramientas lo ven. + +> Los nombres de archivo de reglas de cada herramienta evolucionan rápido — si alguno no funciona, verificá la doc oficial vigente de esa herramienta. El patrón ("apuntá su archivo de contexto a `SKILL.md`") se mantiene. + +--- + +## 5. Higiene de skills en este repo (ahorro de tokens) + +Este proyecto usa **una skill versionada** (`dev-protocol`) más **una opcional local** (`grilling`, stress-test de planes). El resto del pack Matt Pocock / skills genéricas (**tdd**, **review**, **diagnosing-bugs**, **setup-pre-commit**, etc.) fue **archivado** en `.agents/skills/_archive/` porque duplican módulos de `dev-protocol` o no aplican al stack (`uv` + `pre-commit` Python, no Husky). + +### Qué symlinkear en `.claude/skills/` (por clon) + +Solo dos enlaces — **no** re-symlinkear el pack completo ni skills globales de `~/.agents/skills/`: + +```bash +./scripts/setup-skills.sh +# equivalente manual: +# mkdir -p .claude/skills +# ln -sfn ../../.agents/skills/dev-protocol .claude/skills/dev-protocol +# ln -sfn ../../.agents/skills/grilling .claude/skills/grilling +``` + +### Cursor / skills globales + +Si Cursor indexa `~/.agents/skills/` entero (~1500 entradas), el catálogo `available_skills` consume muchos tokens **antes** de leer código. Mitigación recomendada: + +- Mantener skills **a nivel repo** (este script) y **no** exponer el directorio global completo en la configuración del IDE, o +- Reducir el set global a skills que uses en todos los proyectos (p. ej. solo `dev-protocol`). + +### Mapeo: skill archivada → usar en su lugar + +| Archivada | Usar | +| :--- | :--- | +| diagnosing-bugs | `dev-protocol/debugging.md` | +| tdd | `dev-protocol/code-design.md` | +| review | `dev-protocol/qa-review.md` | +| setup-pre-commit | `dev-protocol/git-workflow.md` + `.pre-commit-config.yaml` del repo | +| domain-modeling | `UBIQUITOUS_LANGUAGE.md` (raíz del repo) | +| qa, request-refactor-plan, … | `roadmap/*.md` Agent Prompts + dev-protocol | diff --git a/.agents/skills/dev-protocol/code-design.md b/.agents/skills/dev-protocol/code-design.md new file mode 100644 index 0000000000000000000000000000000000000000..e0902941642972a66918ec391c5547570e8a5921 --- /dev/null +++ b/.agents/skills/dev-protocol/code-design.md @@ -0,0 +1,68 @@ +# CODEBASE DESIGN AND IMPLEMENTATION PRINCIPLES + +All codebase changes should follow the deep module design principles and a vertical, test-driven implementation workflow. + +--- + +## 1. DEEP MODULE DESIGN + +Aim to design **deep modules**: modules that put a lot of behavior behind a small interface, placed at a clean seam, testable through that interface. + +### 1.1. Glossary of Terms +Do not substitute generic terms (like "component," "service," "API," or "boundary") for these design elements: +- **Module**: Anything with an interface and an implementation (function, class, package, or tier-spanning slice). +- **Interface**: Everything a caller must know to use the module correctly (types, invariants, ordering constraints, error modes, configuration, and performance characteristics). +- **Implementation**: What is inside the module (its body of code). +- **Adapter**: A concrete object/code that satisfies an interface at a seam. Describes the *role*, not substance. +- **Depth**: Leverage at the interface. A module is **deep** when a large amount of behavior sits behind a small interface. It is **shallow** when the interface is as complex as the implementation (avoid shallow modules). +- **Seam**: A place where you can alter behavior without editing in that place (the location of a module's interface). +- **Leverage**: Capability per unit of interface learned (one implementation pays back across N call sites). +- **Locality**: Concentration of change, bugs, and knowledge in one place rather than spreading across callers. + +### 1.2. Design Principles +- **Depth is a property of the interface, not the implementation**: A deep module can be internally composed of small, mockable, swappable parts, as long as they remain private to its implementation. +- **The Deletion Test**: If you delete the module and the complexity vanishes, it was a pass-through (shallow). If the complexity reappears across N callers, it was earning its keep (deep). +- **The interface is the test surface**: Callers and tests cross the same seam. If you need to test past the interface, the module is probably the wrong shape. +- **One adapter = hypothetical seam; Two adapters = real seam**: Don't introduce interfaces or seams unless something actually varies across them. + - *Canonical LLM example*: a **local** model backend and a **remote** API are two adapters behind one provider interface — that is a real seam, so the abstraction earns its keep. A single provider with no alternative does not (yet) justify one. See [SKILL.md](./SKILL.md) §3.2. +- **Design for Testability**: + 1. *Accept dependencies, don't create them* (pass collaborators in). + 2. *Return results, don't produce side effects* where possible (pure computations). + 3. *Small surface area* (fewer methods/parameters = simpler setup and fewer tests). + +--- + +## 2. IMPLEMENTATION WORKFLOW: VERTICAL SLICES + +### 2.1. Tracer Bullets vs Horizontal Slicing (Anti-Pattern) +- **Anti-Pattern (Horizontal Slicing)**: Writing all tests first, then writing all implementation code, or implementing layer-by-layer (e.g. database first, then api, then frontend). This leads to tests that check imagined shapes rather than actual behavior. +- **Correct Approach (Vertical Slicing)**: Implement features via **tracer bullets**. Each issue or step is a thin vertical slice cutting through all integration layers (schema, API, UI, tests) end-to-end. One slice should be demoable and verifiable. + +``` +WRONG (Horizontal): + RED: test1, test2, test3, test4 + GREEN: impl1, impl2, impl3, impl4 + +RIGHT (Vertical): + RED→GREEN: test1 → impl1 + RED→GREEN: test2 → impl2 +``` + +--- + +## 3. TEST-DRIVEN DEVELOPMENT (TDD) + +Tests must verify behavior through public interfaces, not implementation details. + +### 3.1. The TDD Cycle +1. **Planning**: + - Confirm interface changes and behavior priorities with the user. + - List the behaviors to test (not implementation steps). +2. **Tracer Bullet**: + - Write ONE test for the first behavior -> Watch it fail (**RED**). + - Write the minimal code to pass -> Watch it pass (**GREEN**). +3. **Incremental Loop**: + - For each remaining behavior, repeat: Write next test (RED) -> Minimal code to pass (GREEN). Do not anticipate future tests or write speculative features. +4. **Refactor**: + - Extract duplication, deepen modules, and apply SOLID principles. + - **Never refactor while RED**. Get to GREEN first, then refactor, running tests after each step. diff --git a/.agents/skills/dev-protocol/debugging.md b/.agents/skills/dev-protocol/debugging.md new file mode 100644 index 0000000000000000000000000000000000000000..6bd5cb397740e9ca3d368ac044745c804bea579d --- /dev/null +++ b/.agents/skills/dev-protocol/debugging.md @@ -0,0 +1,67 @@ +# STRUCTURED BUG DIAGNOSIS PROTOCOL + +When a bug is reported or tests are failing, follow this 6-phase debugging loop. Do not skip phases unless explicitly justified. + +--- + +## Phase 1: Build a Feedback Loop +Everything else is mechanical. If you have a tight, pass/fail signal that goes red on *this specific bug*, you will find the cause. If not, staring at code will not save you. + +### 1.1. Constructing the Loop (Try in order) +1. **Failing test** at whatever seam reaches the bug (unit, integration, e2e). +2. **Curl / HTTP script** against a running dev server. +3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot. +4. **Headless browser script** (Playwright / Puppeteer) asserting on DOM/console. +5. **Replay a captured trace**: save a network request/payload and run in isolation. +6. **Throwaway harness**: spin up a minimal subset of the system (one service, mocked dependencies). +7. **Property / fuzz loop**: run 1000 random inputs to trigger a flake. +8. **Bisection harness**: automate checking versions to `git bisect run`. +9. **Differential loop**: run same input through old vs new versions and diff. +10. **HITL bash script**: drive human-in-the-loop steps via a structured script. + +### 1.2. Tighten and Verify +- **Tighten the loop**: Make it fast (seconds, not minutes), deterministic (pin time/RNG, isolate network), and sharp (assert on the symptom, not just "didn't crash"). +- **LLM determinism**: When the bug is in a model-touching path, remove the model as a variable first. Mock/stub the provider interface or replay a recorded response; if a live model is unavoidable, pin `temperature=0` and a fixed seed. A loop whose redness depends on sampling noise is not red-capable. See [SKILL.md](./SKILL.md) §3.2. +- **Phase 1 Completion Criterion**: You must establish **one command** that runs unattended, is fast, deterministic, and **red-capable** (successfully triggers and catches this exact bug). +- **Prohibited action**: Do not jump to hypotheses or read code to build a theory before this command exists. No red-capable command, no Phase 2. + +--- + +## Phase 2: Reproduce + Minimise +Run the feedback loop and watch it go red. +1. Confirm the loop produces the **user's exact described symptom** (not a different nearby issue). +2. Shrink the repro to the **smallest scenario that still goes red**. Cut inputs, configuration, and steps one at a time. +3. Done when every remaining element is load-bearing (removing any one element makes the loop go green). + +--- + +## Phase 3: Hypothesise +1. Generate **3–5 ranked hypotheses** before testing any of them. Single-hypothesis generation anchors on the first plausible idea. +2. Ensure each hypothesis is **falsifiable**: state the prediction it makes. + > **Format**: *"If is the cause, then will make the bug disappear / will make it worse."* +3. **Show the ranked list to the user** before testing. If the user is AFK, proceed with testing the top hypothesis. + +--- + +## Phase 4: Instrument +1. Map each probe to a specific prediction from Phase 3. Change **one variable at a time**. +2. **Tag every debug log** with a unique prefix (e.g., `[DEBUG-a4f2]`) so they can be cleaned up in a single grep. +3. For performance regressions, establish baseline measurements (`performance.now()`, query plans) before making modifications. + +--- + +## Phase 5: Fix + Regression Test +1. Write a regression test **before the fix** (only if a correct seam exists). +2. If no correct seam exists (e.g. tests cannot replicate the integration path), note this finding. Codebase architecture is preventing the bug from being locked down. +3. Steps: Turn minimised repro into a failing test -> Watch it fail -> Apply fix -> Watch it pass -> Re-run the Phase 1 loop against the original un-minimised repro. + +--- + +## Phase 6: Cleanup + Post-Mortem +Before declaring a bug resolved, complete this checklist: +- [ ] Original repro no longer reproduces (re-run Phase 1 loop). +- [ ] Regression test passes (or absence of seam is documented). +- [ ] All tagged `[DEBUG-...]` instrumentation logs are removed. +- [ ] Throwaway prototypes/harnesses are deleted. +- [ ] Correct hypothesis is documented in the commit/PR message. +- [ ] **Post-Mortem**: Ask: *What would have prevented this bug?* If it requires architectural improvements (better test seams, less coupling), log a task or report it to the user. diff --git a/.agents/skills/dev-protocol/documentation.md b/.agents/skills/dev-protocol/documentation.md new file mode 100644 index 0000000000000000000000000000000000000000..1bc935a80186beb7c3ec0aa2037f2dafce7070cc --- /dev/null +++ b/.agents/skills/dev-protocol/documentation.md @@ -0,0 +1,57 @@ +# DOCUMENTATION SYNCHRONIZATION WORKFLOW + +Update documentation **only when the change actually affects that asset**. Do not touch every file on every micro-fix — that wastes tokens and creates noise. + +These documents are the recovery core of the codebase. Keep them accurate; do not keep them busy. + +| File | Update when… | Do **not** update when… | +| :--- | :--- | :--- | +| **`manifest.json`** | Version bumps, or `state_schema` / `constraints` change (new config field, default, range, or vector dim). | Pure refactors, bug fixes that leave the config contract unchanged. | +| **`CHANGELOG.md`** | Releases or **notable** capability changes (new provider, new surface, security posture change). Append a short section. | Every PR, typo fix, or internal cleanup. | +| **`architecture_spec.md`** | Contracts change: API shapes, provider interface, data schemas, security/scalability policies, prompt/templating contracts, token/latency expectations. | Implementation details that stay within an existing contract. | +| **`README.md`** | How to install, configure, or run the system changes (tooling, scripts, prerequisites). | Internal code changes that do not affect first-time setup. | +| **`CONTEXT.md`** | Domain language changes (new term, renamed concept, retired alias). | Code-only changes that use existing terms. | +| **`current-research/`** | Accidental or planned **empirical discoveries** (measurements, geometry studies, “why does knob X feel dead on model Y?”) that outgrow a one-line lesson. | Routine bug fixes; put the distilled invariant in `lessons-learned.md` and link here for the evidence dump. | +| **`lessons-learned.md`** | A durable engineering **invariant** agents must not re-break. Keep it short; point to `current-research/` for long evidence. | Dumping full measurement tables or open science threads into the skill. | + +### Manifest shape (slim) + +`manifest.json` holds **current state only**: + +- `project`, `version` +- `state_schema` (live config contract) +- `constraints` (e.g. `vector_dim`) + +It is **not** a historical feature-flag ledger. Capability history lives in `CHANGELOG.md`. + +### Agent handoff bundle + +`./run_pack.sh` generates `context.txt` (gitignored) for external LLMs/agents. After meaningful doc or runtime changes, regenerate it when you need a fresh handoff — it is not a committed asset and does not need doc-sync on every change. + +--- + +## CONTEXT & BLUEPRINT WORKFLOW + +By default, the codebase domain context is managed in a glossary format to ensure clean domain definitions. However, a specialized blueprint mode exists for tracking codebase layout. + +### 1. CONTEXT.md (Standard Run - Default) +Behaves strictly as a **Domain Model Glossary & Ubiquitous Language** reference, devoid of code or implementation details. + +- **Structure**: Define terms precisely under subheadings. Keep definitions tight (1-2 sentences max defining what a concept *is*, not what it *does*). +- **Aliases**: Be opinionated. If multiple words exist for the same concept, pick the canonical term and list the others under an `_Avoid_` section. +- **Relationships**: Show bold term names and express cardinality/relationships between concepts. +- **Context Mapping**: For repositories with multiple subdomains or modules, a `CONTEXT-MAP.md` at the root must map out each context (e.g. `[Ordering](./src/ordering/CONTEXT.md)`) and their relationships. +- **Update Frequency**: Update terms inline as decisions are made; do not batch glossary updates. Skip entirely if no domain terms changed. + +### 2. CONTEXT.blueprint.md (Bootstrap Run) +The **Bootstrap Run** is a specialized mode containing a comprehensive codebase re-generation blueprint. + +- **Activation**: Activated in one of two ways: + 1. Setting `"bootstrap_run": true` in `manifest.json`. + 2. Explicit declaration by the user in the prompt. +- **Output File**: Write/update the blueprint in a separate file: **`CONTEXT.blueprint.md`**. +- **Content Requirements**: + - File-by-file inventory and mapping. + - Complete structural dependencies between modules. + - Technical debt analysis and context anchors. + - Absolute context density to enable repository regeneration from zero. diff --git a/.agents/skills/dev-protocol/git-workflow.md b/.agents/skills/dev-protocol/git-workflow.md new file mode 100644 index 0000000000000000000000000000000000000000..2645190e14823f8c2f5824e7df07b6089ca0360f --- /dev/null +++ b/.agents/skills/dev-protocol/git-workflow.md @@ -0,0 +1,76 @@ +# GIT AND VERSION CONTROL WORKFLOW (Gitstuff) + +Follow these rules for committing code, running hooks, and maintaining version safety. + +--- + +## 1. GIT METADATA BLOCK + +Upon successful completion of a logical task, always append a dedicated Git Metadata block at the absolute end of your response using the following format: + +```yaml +Branch Name: / # e.g., feat/provider-fallback or fix/client-timeout-retry +Commit Message: (): # e.g., feat(llm): add local-to-remote provider fallback +``` + +--- + +## 2. PRE-COMMIT HOOK CONVENTIONS + +To keep code quality and formatting consistent before any commit is finalized, use the Python **`pre-commit`** framework (configured via a `.pre-commit-config.yaml` at the workspace root). This replaces Node-centric tooling (Husky / lint-staged) for Python projects. + +A ready-to-use base config ships with this protocol at [`templates/.pre-commit-config.yaml`](./templates/.pre-commit-config.yaml) — copy it to the workspace root and pin the `rev:` tags. + +### 2.1. Recommended Hook Setup +Conventions (swappable per app, but stay consistent within a repo): +- **Format + Lint**: `ruff format` and `ruff check --fix` on staged files (fast, autofixing). +- **Type Check**: a static type check (`mypy` or `pyright`) in CI or as a manual-stage hook (type checking the whole project can be slow for a per-commit hook). +- **Secret Scan**: a secret-detection hook (e.g. `detect-secrets` / `gitleaks`) to enforce the "no keys in git" rule from [SKILL.md](./SKILL.md) §3.2. +- **Hygiene**: trailing-whitespace, end-of-file-fixer, and a check that `.env` is never staged. + +### 2.2. Installation & Smoke Testing +- Install the git hook once per clone: `uv run pre-commit install`. +- Always smoke-test locally before pushing or resolving a task: `uv run pre-commit run --all-files`. +- For a JS/TS frontend that exists alongside (see [SKILL.md](./SKILL.md) §3.3), wire its own formatter via that ecosystem's tooling; do not impose Python hooks on JS files or vice versa. + +--- + +## 3. GIT DELIVERY & AUTOMATION POLICY + +The agent **owns the full git lifecycle and executes it automatically** — branch, stage, commit, push, and merge to the base branch — gated by a single mandatory human checkpoint. Push and merge are **allowed**; they are not blocked. + +### 3.1. The Approval Gate (mandatory) +- The agent may **freely** create branches, stage files, and commit **locally** at any point while working. +- The agent must **NOT `git push` and must NOT merge to the base branch** until the user has verified the change and given an **explicit go-ahead** (e.g. "ok", "dale", "andá", "mergealo"). +- Reporting "ready to test" and then **waiting** is mandatory. Silence, a thumbs-up on something unrelated, or the absence of objection is **not** approval. + +### 3.2. Delivery Sequence (run only after approval) +Default sequence once the user approves: +1. `git checkout -b /` — if not already on a dedicated task branch. +2. Stage **only files relevant to the task**. Leave unrelated untracked/modified files alone; if scope is unclear, ask (see §3.3). +3. `git commit` using the metadata format from §1, ending with the `Co-Authored-By` trailer. +4. `git push -u origin `. +5. `git checkout ` → `git merge --no-ff ` → `git push origin `. (`` is usually `main`.) +6. *(Optional, ask first)* delete the merged branch locally and on the remote. + +> Alternative: if the repo works through pull requests, substitute steps 4–5 with `gh pr create` + merge. Default to the direct merge above unless the user or repo conventions say otherwise. + +### 3.3. Stop-and-Ask Conditions ("when it gets complicated") +**Pause and ask the user** before continuing if any of these arise during delivery: +- Merge conflicts, or the base branch has diverged / moved since branching. +- A pre-commit hook, type check, test, or CI check **fails**. +- The base branch is **protected**, or the push is rejected. +- A **force-push** (`--force` / `--force-with-lease`) would be required. +- Commit **scope is ambiguous** (unrelated changes staged, or unrelated untracked files present that might belong in the commit). +- The remote, credentials, or target branch are **not what was expected**. + +### 3.4. Destructive Commands (always require explicit confirmation) +These are **not** part of the normal flow and risk irreversible data loss. Never run them autonomously — propose the command and get an explicit "yes" first: +- `git reset --hard` (prefer a soft reset or `git restore `). +- `git clean -f` / `git clean -fd`. +- `git branch -D`. +- `git checkout .` / `git restore .` (reverting the entire working directory). +- Any history rewrite on an already-pushed branch (`rebase`, `commit --amend` after push, force-push). + +### 3.5. Claude Code Integration +Optionally register a `PreToolUse` matcher hook (e.g., `.claude/hooks/block-dangerous-git.sh`) that intercepts **only the §3.4 destructive commands**. `git push` and `git merge` must **not** be blocked — they are governed by the approval gate (§3.1), not by a hook. diff --git a/.agents/skills/dev-protocol/lessons-learned.md b/.agents/skills/dev-protocol/lessons-learned.md new file mode 100644 index 0000000000000000000000000000000000000000..925165c9e585fe6abe2607867ed36e6a14484f5d --- /dev/null +++ b/.agents/skills/dev-protocol/lessons-learned.md @@ -0,0 +1,508 @@ +# LESSONS LEARNED & ARCHITECTURAL INVARIANTS + +Este archivo registra las lecciones aprendidas, invariantes de arquitectura y patrones de ingeniería descubiertos en el desarrollo del proyecto. Debe ser **revisado, consultado y actualizado continuamente** por los agentes de IA en cada ciclo de trabajo del `dev-protocol`. + +--- + +## 1. WebGL & GPU Shaders (Three.js) + +### 1.1. Desactivación de Frustum Culling (`frustumCulled = false`) +- **Problema**: Three.js descarta automáticamente objetos fuera de la vista calculando una esfera delimitadora (`boundingSphere`) al instanciar. Al modificar buffers `Float32Array` in-situ o al mover/estirar la cámara, las mallas de puntos y líneas desaparecen repentinamente. +- **Solución Obligatoria**: Fijar `frustumCulled = false` en todas las mallas de puntos y líneas: + ```javascript + pointsMesh.frustumCulled = false; + lineMesh.frustumCulled = false; + ``` + +### 1.2. Transparencia y Profundidad (`depthWrite = false`, `transparent: true`) +- **Problema**: Mallas de puntos con opacidad dinámica solapadas se bloquean entre sí si la memoria de profundidad (*depth buffer*) bloquea píxeles traseros. +- **Solución Obligatoria**: Desactivar escritura de profundidad en el material: + ```javascript + transparent: true, + depthWrite: false, + blending: THREE.NormalBlending + ``` + +### 1.2c. Sin GridHelper de referencia en el piso +- **Problema**: `GridHelper(800, 80)` bajo la escena competía visualmente con RIBBONS/COMPARE (cuadrícula visible a lo lejos). +- **Solución Obligatoria**: No montar grid en `SceneSetup` — fondo + fog bastan; el AxisGizmo del dock cubre orientación. +- **Invariante**: no reintroducir grid de piso sin pedido explícito. + +### 1.2b. Fog de escena vs RIBBONS (no confundir con depthWrite) +- **Problema**: `FogExp2(0x050505, 0.008)` oscurecía `MeshBasicMaterial` (RIBBONS, `fog: true` por default) según distancia a cámara — a poses COMPARE (~Z 390) las cintas casi negras y la “sombra trepa” al orbitar. POINTS (shader custom sin fog chunks) no se veían igual. +- **Solución Obligatoria**: Densidad suave `SCENE_FOG_DENSITY = 0.0008` (`SceneSetup.js`) — atmósfera leve, legible a ~400u (`exp(-d·dist) > 0.5`). **RIBBONS**: fog desactivado (`setFogForRenderMode` / `shouldEnableSceneFog`) para probar saturación total a distancia. No es lighting ni colormap. +- **Invariante**: Si reaparece oscurecido al mover cámara en RIBBONS, revisar fog **antes** que normales/luces. PR aparte: solapamiento por `depthWrite: false` en ribbons. +- **Seguimiento**: RIBBONS ya no monta `basePlane`. Compare+RIBBONS no monta POINTS (bug: `else if (pointsData.length)` atrapaba RIBBONS). `depthWrite` / opacidad de wide ribbons → si reaparece solapamiento al orbitar. + +### 1.3b. Galaxy soft-star POINTS (opt-in) +- **Problema**: Square Chebyshev `gl_PointCoord` mask looks tiled when Galaxy points are dense/small. +- **Solución**: Soft-star path (`resolvePointEdgeStyle` / `galaxy: true`) — solid circular core + soft halo; higher size attenuation with smaller clamp. ANALYSIS/NAV keep §1.3 / §1.5 squares. +- **Invariante**: only Galaxy POINTS; `frustumCulled = false` unchanged. + +### 1.3. Renderizado de Puntos Sólidos y Definidos (Sin Halos Esfumados) +- **Problema**: Un gradiente suave amplio de `smoothstep(0.0, 0.5, dist)` genera puntos borrosos, translúcidos y "esfumados". +- **Solución Obligatoria**: Renderizar discos sólidos con un borde de anti-aliasing ultra-definido de 1 píxel: + ```glsl + float solidEdge = 1.0 - smoothstep(0.44, 0.49, dist); + vec3 finalColor = color * solidEdge; + float alpha = dynamicAlpha * solidEdge; + ``` + +### 1.4. Limitación de Grosor de Líneas en WebGL +- **Problema**: La especificación WebGL sobre ANGLE/macOS/Windows limita `LineBasicMaterial.linewidth` a máximo 1px. +- **Solución Obligatoria**: Escalar el grosor visual mediante el tamaño de los puntos (`pointSize` en `ShaderMaterial`), los cuales sí escalan correctamente en GPU con `sizeAttenuation: true`. + +### 1.5. Renderizado de Puntos Cuadrados/Cúbicos GLSL y Línea Base de Origen +- **Invariante**: Para renderizar puntos cuadrados/cúbicos nítidos en GPU, se calcula la distancia Chebyshev `max(abs(coord.x), abs(coord.y))` y se aplica suavizado de borde de 1 píxel `smoothstep(0.44, 0.49, maxDist)`. +- **Línea Base en Análisis**: En el modo **Análisis**, se renderiza una malla de línea vertical (`THREE.Line`) con opacidad de cristal (`opacity: 0.6`, `transparent: true`, `frustumCulled = false`) anclando el inicio ($X = \text{startX}$) de todos los hilos vectoriales apilados. + +### 1.6. Dim Ruler — regla dimensional básica (conector cross-token) + +**Regla canónica (no negociable):** el Ruler es un **conector de la misma dimensión entre tokens**. En cada dim *pintada* *k* une el punto de esa dim del token 1 → token 2 → token 3 → … (orden de lista). + +``` +dim k: T1.dim_k ─── T2.dim_k ─── T3.dim_k ─── … ─── Tn.dim_k +``` + +Misma X (misma dim), a lo largo de Y (y Z en Navigation). **No** une dim→dim a lo largo de X. + +**Cursor de pintura (no “llenar hasta N”):** +- El número del panel es el **cursor**: `+` **pinta en esa dim** y avanza (+1); `−` **borra en esa dim** y retrocede (−1). +- Las dims ya pintadas **quedan** si saltás el cursor (ej. pintaste 1…5, vas a 78 y `+` → quedan 1…5 **y** 78). +- Para borrar: ponés el cursor en la dim (ej. 5) y `−`. +- Estado: set sparse `rulerPaintedDims` (1-based). `rulerLineCount` = `painted.length` (display). Legacy `lineCount` solo → migra a `1..N`. + +| | Correcto | Incorrecto (regresiones) | +| :--- | :--- | :--- | +| Geometría | token→token en cada dim pintada | envolvente max-Y uniendo dim1→dim2→dim3 | +| Mesh | solo `LineSegments` + `LineBasicMaterial` | joint `Points` / `PointsMaterial` + `sizeAttenuation` → **cuadrados blancos enormes** | +| UI / estado | cursor de pintura + set sparse | fill contiguo `1..lineCount` / toggle Path/Span | + +- **Dónde**: COMPARE + ARITHMETIC en ANALYSIS y NAVIGATION. **Nunca** en Galaxy. +- **Persistencia**: `rulerCursor`, `rulerPaintedDims`, `rulerLineCount`, color, thickness en `vl3d.viz.*`. +- **Código**: `dimRuler.js` (`addDimRulerLine` / `removeDimRulerLine` / `buildDimRulerSegments`) → `Instancer._buildDimRulerMesh` → `MeshFactory.createDimRulerMesh`. +- **Lección**: si el ruler “se ve raro”, verificar segmentos **entre tokens en la misma dim**, set sparse (no fill), y sin `Points` de joints. + + +--- + +## 2. Visualización y Normalización de Embeddings LLM + +### 2.1. Estandarización Z-Score + Tanh +- **Problema**: Los vectores de embeddings de LLMs (`all-mpnet-base-v2`, OpenAI, etc.) tienen magnitudes absolutas pequeñas ($v_i \in [-0.15, +0.15]$). Dividir directamente por el valor máximo genera puntos oscuros e invisibles (5% de opacidad). +- **Solución Obligatoria**: Calcular la media ($\mu$) y desviación estándar ($\sigma$) del dataset y aplicar compresión sigmoidal simétrica: + ```javascript + export function calculateZScoreNormalized(values, scaleFactor = 1.2) { + const mean = values.reduce((a, b) => a + b, 0) / values.length; + const std = Math.sqrt(values.reduce((sq, n) => sq + Math.pow(n - mean, 2), 0) / values.length) || 1.0; + return values.map(v => Math.tanh(scaleFactor * ((v - mean) / std))); + } + ``` + +### 2.2. Rampas Cromáticas Divergentes (Color Anchors) +User-editable hex anchors replace the former fixed dual ramp (no product mid-stops at ±0.5 orange/blue): +- **+1** default `#FFE600`, **0** `#000000`, **−1** `#9900E6`. +- Interpolation: lerp(`zero`, `positive`, t) for t≥0; lerp(`zero`, `negative`, −t) for t<0. +- POINTS shader: uniforms `uColorPos` / `uColorNeg` / `uColorZero` (never hardcode ramp in GLSL). +- Sign filter + colors are **global** (`vl3d.viz.*` in localStorage); filter runs on **normalized t** (ε=0.01), not raw dims. +- Continuity lines use `LineSegments` + index pairs; wide ribbons omit quad indices for filtered pairs (keeps full vertex buffers for compare reorder). + +### 2.3. Continuidad de hilo según RENDER mode (mutuamente excluyentes) +- **POINTS**: puntos + línea fina `createRibbonMesh` (continuidad 1px). +- **RIBBONS**: la continuidad **es** la cinta ancha (`createWideRibbonMesh`); sin plano base (el quad oscuro se veía a través de cintas transparentes). No usar `LineBasicMaterial.linewidth` (§1.4). +- Colormap RIBBONS: rampa **divergente** VHectorLab (anchors) para consistencia de marca. +- **Retired**: `RENDER: MESH` (surface heightfield) removed in v1.6.0 — `normalizeRenderMode('MESH')` → POINTS. Do not reintroduce without an explicit product decision. + +### 2.4. Optimización de Fragment Shader para Cero Activación ($|t| < 0.01$) +- **Patrón**: Evitar cálculos de interpolación `mix()` en fragmentos con intensidad casi nula. +- **Solución Obligatoria**: Evaluar $|t| < 0.01$ al inicio del Fragment Shader y realizar un early return con color negro y opacidad mínima ($\alpha \approx 0.05$): + ```glsl + if (absT < 0.01) { + gl_FragColor = vec4(vec3(0.0), 0.05 * baseOpacity * solidEdge); + return; + } + ``` + +--- + +## 3. Navegación y Controles 3D (WASDQE) + +### 3.1. Inercia Acotada sin Acumulación Exponencial (`lerp`) +- **Problema**: Sumar aceleración en cada frame (`velocity.add(moveVector)`) con amortiguación `velocity.multiplyScalar(damping)` causa una acumulación exponencial de velocidad (hasta $\frac{1}{1-\text{damping}} \approx 8.33\times$ la velocidad base), haciendo que la cámara salga disparada tras presionar 'W' por medio segundo. +- **Solución Obligatoria**: Acotar la velocidad mediante interpolación lineal directa al vector objetivo: + ```javascript + if (moveVector.lengthSq() > 0) { + moveVector.normalize().applyQuaternion(this.camera.quaternion).multiplyScalar(targetSpeed); + this.velocity.lerp(moveVector, 0.25); // Velocidad acotada y constante + } else { + this.velocity.multiplyScalar(this.damping); // Desaceleración suave + } + ``` + +### 3.2. Aislamiento de Teclado durante Entrada de Texto en UI +- **Problema**: Al escribir en inputs HTML de la UI (ej. barra de búsqueda de palabras), las teclas 'W', 'A', 'S', 'D' mueven la cámara 3D involuntariamente. +- **Solución Obligatoria**: Filtrar eventos de teclado si el elemento activo es un input de formulario, y resetear la velocidad en el evento `blur`: + ```javascript + const tag = document.activeElement ? document.activeElement.tagName : ''; + if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; + ``` + +### 3.3. Vista de Análisis de Frente y Proyección de Etiquetas Flotantes +- **Patrón**: Proyectar los orígenes 3D ($X=0$) de los hilos vectoriales a coordenadas 2D de pantalla (`vector.project(camera)`) para renderizar cartelitos Glassmorphic anclados al inicio de cada hilo. +- **Invariante**: En el modo **Análisis**, los hilos se apilan verticalmente a lo largo de $Y$ con encuadre frontal directo (`Z=360`), permitiendo visualizar todos los componentes y el resultado inmediatamente sin desplazamientos manuales. + +- **Invariante**: ANALYSIS/NAV flight numbers unchanged; no Spatial speed knobs in v1. + +### 3.5. Touch mobile: joystick/look no roban eventos de docks/HUD +- **Problema**: Pointers que empiezan en drawers/Compare/HUD mueven la cámara o pelean con scroll. +- **Solución Obligatoria**: Look solo si `target` es `CANVAS` y no hace match de docks/HUD/joystick (`isUiTouchTarget`). Joystick y botones Q/E escriben ejes en `Navigation` (`setMoveAxes` / `setVertical`); el update sigue el mismo `lerp` §3.1. Desktop mouse/WASD intacto. +- **Invariante**: scroll en `.compare-cosine-list` no mueve cámara; touch UI ≠ look. + +### 3.4. Default poses by MODE|VIEW|RENDER +- **Startup default**: **ARITHMETIC | ANALYSIS | POINTS** (`appViewDefaults.js`). Navbar + `main.viewMode` + empty `resolveCameraPose` / `resolveSpatialDefaults` ctx follow that triad. +- **Invariante**: Al cambiar MODE / VIEW / RENDER se reaplica cámara (`resolveCameraPose` → `Navigation.setContextView`) + sliders (`resolveSpatialDefaults` → sync UI). Claves `MODE`, `MODE|VIEW`, `MODE|VIEW|RENDER` (más específico gana). +- **Fallbacks de VIEW** (sin override): NAVIGATION corridor `POS (-178.3, 13.5, 52.2)` / `ROT (-5.4°, -51.5°, 0°)`; ANALYSIS `POS (-75.2, -0.8, 62.5)` / `ROT (0°, 0°, 0°)`. +- **Overrides capturados**: + - `ARITHMETIC|ANALYSIS|POINTS` — sliders Amplitud $Y=40$, Grosor $0.05$; cámara vía fallback ANALYSIS. + - `COMPARE|NAVIGATION|POINTS` — sliders Spacing `0.7`, Dist Y `10`, Amp `4.9`, Length `0.1`, Thickness `0.01`; cámara `POS (-106.5, 20.4, 390.2)` / `ROT (-3.9°, -8.4°, 0°)`; primer load COMPARE usa `COMPARE_AUTO_PRESETS.default` (lexicón completo), no `sample5`. + - `COMPARE|NAVIGATION|RIBBONS` — sliders Spacing `1.55`, Dist Y `10`, Amp `7`, Length `0.057`, Thickness `0.05`; cámara `POS (-575.8, 43.8, 237.9)` / `ROT (-22.4°, -35.7°, 0°)`; fog **off** en RIBBONS (`shouldEnableSceneFog`). +- **Overlay de captura**: El HUD `CAM POSE` solo se monta si `VITE_SHOW_CAM_POSE=true` (default `false` en `.env.example`). +- **Workflow de captura**: Activar overlay → navegar a la pose → screenshot POS/ROT → actualizar `CAMERA_DEFAULT_OVERRIDES` y/o `SPATIAL_DEFAULT_OVERRIDES` para la clave `MODE|VIEW|RENDER`. + +### 3.6. Galaxy flight profile (VIEW-only) +- **Problema**: UMAP world + default `moveSpeed` 75 / look `0.003` felt cramped and twitchy in Galaxy. +- **Solución**: `galaxyFlightProfile.js` — apply on enter Galaxy, restore defaults on leave. Calibrated: `moveSpeed` 56 (former Shift feel), `turboMultiplier` 2, `lookSensitivity` 0.0014. World scale via `resolveGalaxyWorldScale` (`GALAXY_DEFAULT_SCALE` 96, spacing×240). +- **Invariante**: ANALYSIS/NAV flight numbers unchanged; no Spatial speed knobs in v1. + +### 3.7. Galaxy `/project` with 1–2 tokens (UMAP floor) +- **Problema**: Token Comparison accepts 1–1024 tokens, but UMAP needs ≥3 samples. Visualize of a pair (e.g. `White, Blanco`) in VIEW GALAXY hit `UMAP requires at least 3 vectors` → pipeline aborted after encode → cosine list / scene unchanged (“nothing happens”). +- **Solución Obligatoria**: `backend/projection.py` — for `n < 3`, deterministic micro-layout (`origin` / `pca_micro`) instead of rejecting; meta reports `fallback`. Real UMAP only for `n ≥ 3`. +- **Invariante**: `/project` must succeed for any legal Compare cardinality (1..1024); never hard-fail small-n Galaxy Visualize. + +--- + +## 4. UI Panels (Sidebar / Compare) + +### 4.1. Panel Arithmetic — Top-10 siempre alcanzable (scroll en la lista) +- **Problema histórico**: scrollbar del panel entero era molesta; se comprimió el Top-10 con `overflow: hidden` en `.results-list` → en viewports bajos se **cortaban** vecinos sin forma de verlos. Fórmulas `calc(100vh - 380px)` podían dar **≤0** en landscape. +- **Solución Obligatoria (actual)**: + - Tall: `#sidebar-panel` `overflow: hidden`; `#sidebar-panel .results-list` `overflow-y: auto` + `max(120px, min(280px|200px, 40dvh|36dvh))` — **floor 120px**, barrita al costado OK. + - Short (`max-height: 560px`): el **panel** scrollea (form + Top-10); lista sin max-height clip — escape hatch cuando el dock es chico. + - Dock body: `max-height` con `100dvh − navbar − HUD`, no magic `100vh - 160`. + - Contrato: `arithmeticResultsScroll.js` (`resolveResultsListMaxHeightPx`, `shouldScrollArithmeticPanel`). + - Items Top-10 **solo lectura** (`pointer-events: none`). +- **Invariante**: Top-10 siempre alcanzable (lista o panel); no es control de navegación 3D. + +### 4.1b. Mobile chrome MQ — width OR short landscape phone +- **Problema**: iPhone landscape suele tener `width > 768` → salía del path mobile (sin joystick, sin densidades, list heights desktop inútiles). +- **Solución Obligatoria**: `MOBILE_MQ = (max-width: 768px), ((max-height: 500px) and (hover: none))` en CSS + `CollapsibleDock.isMobileViewport`. Compact form Arithmetic bajo `max-height: 500px`. `viewport-fit=cover` en `index.html` para safe-area. +- **Invariante**: phone landscape short = mobile chrome; tablet portrait alto ≠ phone. + +### 4.2. COMPARE Cosine-vs-Anchor: scroll interno + reorder 3D con tween in-situ +- **Problema**: Listas largas (20/50/1024) en Compare rompen el layout si el scroll vive solo en el panel; un reorder con `clear()` + rebuild provoca flicker y desync lista↔3D. Con form SAE alto + cap `min(68vh, 600px)`, un `.compare-results { overflow: hidden; flex: 1; min-height: 0 }` **clippea** el header/lista cosine debajo de ACTIVE SEQUENCE METRICS — queda espacio vacío y la lista es inalcanzable incluso scrolleando el panel (el overflow del hijo no participa del scroll del padre). +- **Solución Obligatoria**: + - Panel Compare: `overflow-y: auto` + max-height HUD-capped (form + SAE + métricas caben vía scroll de panel). + - `#compare-panel .compare-results`: `flex: 0 0 auto; overflow: visible` — el bloque métricas+cosine **participa** del scroll del panel; nunca `overflow: hidden` que trague la lista. + - Scroll **de filas** en `#compare-panel .compare-cosine-list` (`overflow-y: auto` + `max-height` + **`min-height: 120px`** floor); la lista no puede colapsar a 0px. + - Filas de similitud: `pointer-events: none` en la fila; solo ▲/▼ con `pointer-events: auto` (métrica + reorder, sin focus de cámara). + - Reorder: recalcular `cosine_vs_first` en memoria (`compareCosine.js`); animar slots con `Instancer.animateCompareReorder` (lerp de `sequenceIndex` fraccionario ~200–400ms, reuse de ribbon/points meshes, `ThreadLabels.updateOrigins` por frame). + - Bloquear spamming de flechas mientras `_reorderBusy` (un tween a la vez). +- **Invariante**: lista COMPARE ↔ orden de hilos 3D siempre sincronizados; #1 es ancla REF con score `1.0000`; cosine list siempre alcanzable bajo ACTIVE SEQUENCE METRICS. + +### 4.3. Docks colapsables: transform + tab, sin desmontar DOM; MODE comparte estado izq. +- **Problema**: Ocultar paneles con `display: none` / desmontar nodos pierde estado de formularios, sliders y lista cosine; duplicar lógica left/right y resetear collapse al cambiar MODE rompe la UX. +- **Solución Obligatoria**: + - Host único `CollapsibleDock` por borde: slide con `transform` (~250ms); hijos permanecen montados. + - Colapsado: `pointer-events: none` en `.dock-body`; la pestaña (`.dock-tab`) sigue `pointer-events: auto` y expone `aria-expanded`. + - **Dock izq. / MODE**: un solo flag collapsed (y una key `localStorage`) para Arithmetic|Compare; el cambio de MODE solo alterna `.hidden` del panel activo dentro del body — no toca collapse. + - **Dock der.**: sliders + AxisGizmo viven en el mismo host; el HUD inferior de telemetría **nunca** entra al dock (siempre visible). + - Desktop: persistir collapsed en `localStorage`. Mobile (`max-width: 768px`): default collapsed y no persistir (hook Etapa B). + - El dock host mantiene `overflow: hidden` / `fit-content` — scroll interno vive en listas (§4.1 Top-10 / §4.2 cosine), no en el dock. +- **Invariante**: collapse ≠ unmount; MODE no resetea el dock izquierdo; HUD bottom ∉ docks. + +### 4.3b. Boot / Galaxy progress overlay +- **Problema**: El strip `#galaxy-progress` vive en el dock izquierdo (a menudo colapsado, o el panel Compare aún oculto al venir de ARITHMETIC) → canvas vacío sin feedback al entrar a Galaxy / cold start. +- **Solución**: `BootProgress` overlay fixed full-viewport (`#boot-progress`) + mirror al strip del Compare panel; `withSoftStepProgress` mueve la barra *dentro* de cada paso del pipeline. +- **Invariante**: progreso de Galaxy siempre visible sobre el canvas; no depender del dock. + +### 4.5. Control Espacial 3D — defaults = mid del rango + steps finos + dblclick reset +- **Problema**: Rangos históricos asimétricos (Distancia Y / Grosor con default = min; Amplitud hasta 240) dejan el thumb pegado a un extremo al load — solo se puede agrandar, no afinar alrededor del punto dulce. Steps gruesos (enteros / 0.1 en rangos chicos) impiden valores intermedios. Sin gesto de reset, volver al punto dulce exige adivinar el valor. +- **Solución Obligatoria**: Un solo set global de min/max (mismo track en todas las combos MODE|VIEW|RENDER). Separación $X$ ∈ $[0.4,2.0]$ step $0.05$ (COMPARE default $0.7$ se conserva; global $0.4$ en el min). Distancia $Y=10$ ∈ $[1,19]$ step $0.1$, Amplitud $Y=7$ ∈ $[1,40]$ step $0.1$ (**asimétrico**: default global 7 y override Analysis 40 se conservan; COMPARE Amp $4.9$ no fuerza mid). Longitud $Z$ ∈ $[0.001,0.2]$ step $0.001$ (nunca 0; COMPARE $0.1$ ≈ mid; global $0.2$ en el max; label 3 dec). Grosor $=0.05$ ∈ $[0.01,0.09]$ step $0.01$. Labels: 2 dec (X/Grosor), 3 dec (Z), 1 dec (Y). Defaults vía `resolveSpatialDefaults` / overrides por clave. **Doble clic** restaura solo ese slider al default del contexto. +- **Invariante**: rangos idénticos en todas las combos; defaults por combo se conservan; no todos los defaults son mid lineal; Length nunca llega a 0. + +### 4.6. ThreadLabels 3D — Arithmetic corto; Compare = tokens ingresados +- **Problema**: Badge de `type` + texto largo (`WORD_A` + `VECTOR A`, `#1 COS VECTOR (queen)`) hinchaba las cards glassmorphic en Análisis Arithmetic. +- **Solución Obligatoria**: + - **Arithmetic**: un solo span vía `arithmeticThreadLabel` → `WORD_A|WORD_B|WORD_C|RES|TOP1`. Sin badge de tipo; `res-label` / `top1-label` por `type`. + - **Compare**: el texto de la label 3D es el **token ingresado completo** (como antes). No prefijar `TOPn`, no truncar. La lista Compare / reorder sigue mostrando todos los items. +- **Invariante**: no mezclar políticas Arithmetic↔Compare; labels 3D ≠ paneles laterales (Top-10 / cosine list). +- **Lección**: acortar labels en un mode no implica el mismo formato en el otro — validar Compare aparte antes de unificar. + +### 1.6. RENDER: MESH retired (v1.6.0) +- **Decisión**: surface heightfield (`createSurfaceMesh`) removed from product — no aportaba vs POINTS/RIBBONS y costaba mantener. +- **Invariante**: navbar + runtime = POINTS | RIBBONS only. Legacy `"MESH"` → POINTS via `normalizeRenderMode`. No reintroduce surface mode without explicit ask. + +### 1.7. RIBBONS = wide mesh strips (nunca Line linewidth; sin base plane) +- **Problema**: `LineBasicMaterial.linewidth` queda capado a 1px en WebGL (§1.4); no sirve para cintas anchas de referencia. Un `createBasePlane` semitransparente bajo las cintas se veía como rectángulo oscuro a lo lejos (bordes rectos). +- **Solución Obligatoria**: `createWideRibbonMesh` (quad strip a lo largo del centerline). Sin Points. Sin montar plano base en Instancer. +- **Invariante**: RIBBONS ≠ POINTS; no reintroducir base plane bajo ribbons sin opacificar/`depthWrite` conscientes. + +### 4.4. Landscape Gate — RETIRED (portrait preferred) +- **Antes**: Soft overlay en phone portrait (“Better in landscape”). +- **Decisión**: Retirado — en producto real se ve **mejor en portrait** que de costado; el cartel era mentira UX. +- **Solución Obligatoria**: `shouldShowLandscapeGate` → siempre `false`; clase no monta copy. No reintroducir nudge landscape sin pedido explícito. +- **Invariante**: phone = portrait-first; no orientation lock; render loop nunca pausa por orientación. + +--- + +### 4.7. Product copy = English; identifiers stay +- **Invariante**: Visible UI copy (navbar, panels, sliders, landscape gate, aria/titles/placeholders) is English-only. Internal keys (`data-view="ANALYSIS"`, `threadAmplitudeY`, CSS classes) are not renamed for i18n. No i18n framework — single-language product surface. +- **Compare 3D labels** remain raw input tokens (§4.6); panel chrome uses EN glossary. + +### 4.8. COMPARE groups — parse frontend, flat /compare, badges follow centroids +- **Decisión (v1.7.0)**: `GROUP_name = tokens` en textarea → `parseCompareInput` concatena grupos; `/compare` sigue flat. Anchor = primer token global (D1a). Sort/reorder cosine **global** puede romper contigüidad (D2a); badges de grupo se re-anclan al centroide de miembros actuales. +- **Invariante**: groups = metadata UI/layout, no endpoint nuevo; duplicados entre groups permitidos. +- **Visibilidad**: con groups activos, overlay muestra **solo** badges de grupo (los token cards a N alto los tapaban y el offset grande los metía bajo el dock z-index 40). Lista cosine del panel con ≥2 `groupId`: filas **grupo↔primer grupo** (centroide = media de embeddings L2 + re-L2; similitud cosine); flat / un solo grupo = token vs primer token. Reorder ▲/▼ del panel no aplica en modo grupos (3D intacto). `#thread-labels-container` z-index ≥ docks; offset screen de group badges chico. +- **Bootstrap**: textarea default **y** auto-Visualize al entrar a COMPARE deben salir de `getCompareBootstrap()` (groupsDemo + `tokenMeta`). Autoload de `COMPARE_AUTO_PRESETS.default` (136 flat, sin meta) mientras el textarea muestra `GROUP_*` → etiquetas de token forever. Preset buttons deben Visualize, no solo rellenar texto. +- **SAE + groups**: `applySaeToCompare` y encode path deben re-afirmar `groupId`/`groupLabel` desde RAW cache (no `groupName`). `ThreadLabels.updateOrigins` rebuilds DOM si el set de ids cambia (tokens → group badges). +- **Galaxy cache + regroup**: fingerprint de texts puede pegar cache de `/compare`, pero Visualize con **mismos tokens / grupos distintos** debe re-correr `attachMeta(tokenMeta)` — si no, badges/chips quedan en el groupId viejo. + +### 4.9. Visualization Controls — sign filter + color anchors + zero coverage (v1.8.x) +- **Panel**: glass card glued to the **bottom HUD** (app root); EN copy; global (not per MODE|VIEW|RENDER). Right dock keeps Spatial Controls + AxisGizmo only. +- **Sign filter**: `all | positive | negative` on **normalized t** (post z-score/tanh), ε=`0.01` aligned with shader short-circuit. +/− only hide the opposite sign **and** near-zero. Applies to POINTS (fragment `discard`) **and** continuity `LineSegments` / wide-ribbon **index omission** (F4) — keep full vertex buffers so COMPARE reorder in-situ still works. +- **Color anchors**: user hex +1 / 0 / −1 replace fixed mid-stop ramps; lerp `0↔+1` and `0↔−1`. POINTS shader uses uniforms `uColorPos` / `uColorNeg` / `uColorZero` (never hardcode ramp in GLSL). +- **Zero coverage %**: slider **below Colors, above Reset**. Remaps `|t|` so zero color occupies `coverage` of the ± range before lerp (`remapAbsTWithZeroCoverage`); CPU + `uZeroCoverage` share math. Cap 90% so ±1 remains reachable. Persist `vl3d.viz.*` including `zeroCoverage`. +- **Collapse / HUD glue**: Visualization mounts on **app root**. Short **left** dock-tab (▼ raised / ▲ resting on HUD). Expanded + collapsed share the **same HUD top seam** (right end of the bar via `--hud-pad`). Collapse only hides the body — tab stays parked **on** the HUD (flat bottom, no side orphan slot). Desktop + mobile. Key `vl3d.viz.panelCollapsed`. +- **Bottom HUD width**: tip-to-tip with equal `--hud-pad` insets (navbar-like span, not edge-glued). Right dock / AxisGizmo sit **above** the HUD (`bottom: hud-bottom + hud-height + 8`) so full-width telemetry does not collide with the gizmo. +- **Mobile chrome density**: navbar **single row** `--navbar-height: 28px` (no wrap / no second tab row); tabs ~20px / ~0.55rem; status = green/red **dot only** (hide ONLINE text). Overflow tabs: ◀ ▶ arrows via `navbarTabsScroll.js` (`getTabsScrollState` / `nextTabsScrollLeft`) — arrows only when `.is-overflowing`. HUD strip compacta y fluida al ancho — no forzar 44px en chrome decorativo (inputs/CTAs del panel sí mantienen 44px / 16px iOS zoom guard). Docks `top: calc(var(--navbar-height) + 6px)`. +- **SemVer**: if `main` already shipped a MINOR (e.g. groups `1.7.0`), the next capability must take the **next** MINOR (`1.8.0`) — never reuse a version number already on `main`. +- **Invariante**: filter-on-normalized-t; F4 = geometry not points-only; anchors always drive ramp in v1; no backend for viz controls. + +### 4.10. Top‑K SAE Clean/Denoise (v2.0.0) +- Roadmap: `roadmap/sae-denoise.md`. **Trained** Top‑K Sparse Autoencoder (PyTorch), ported from the predecessor tool — **not** sinusoidal fake projection. +- **Train scope = current workspace batch** (Compare items / Arithmetic vectors), **not** the full vocabulary. Model is **ephemeral** (RAM session); `POST /api/sae/clear` on Visualize/Calculate. +- Defaults caps: 768 → 8192 latents, K=32; `suggest_sae_dims(n)` auto-scales down for small N. Encode activations drive 3D when toggle ON; `/api/sae/status|train|encode|clear`. +- UI: Compare-only 50/50 CTA `Clean/Denoise (SAE)`; replace Compare vectors while ON; raw 768 cache restore on OFF; `vl3d.sae.*` localStorage; train progress via status poll. **Arithmetic has no SAE.** +- **HTML5 form trap**: SAE train params (`type=number`) live **inside** `#compare-form`. Invalid `min`/`step` vs default `lr` (e.g. `lr=0.001` with `min=1e-5` `step=1e-4` → step mismatch) **silently blocks** `type=submit` Visualize — no toast, params panel often hidden so no browser bubble. Presets still work (they call `onCalculate` directly). **Invariante**: `lr` input uses `step="any"`; any new number field in the Compare form must pass an HTML5 step-validity check against product defaults (see `tests/saeControls.test.js`). +- Archived “NO SAE” in `roadmap/archivo/big-picture.md` is **superseded**. +- **Train fast-path**: `from_numpy` + full-batch when N small; MPS/CUDA via `SAE_DEVICE=AUTO`; CUDA AMP; `inference_mode` for final metrics; `suggest_train_schedule` caps epochs (≤12 if hidden≤128). Defaults UI epochs=20. +- Encode already had bucketing + inference_mode + autocast; MPS autocast attempted with FP32 fallback. +- **Encode I/O bottleneck**: GPU matmul ~20ms; dense `.tolist()` of `[N, 8192]` JSON was the freeze. Fix: `encode_vectors_sparse` → `{format:topk_sparse, indices[N,K], values[N,K]}` + router `ORJSONResponse`; `RemoteProvider.saeEncode` densifies via `densifyTopKActivations`. `load_model()` is singleton (`model is not None` short-circuit) — do not `torch.load` per request. +- **Train UI hang**: UI could sit on `Starting SAE training… · working Ns` while backend was already `success`. Causes: (1) poll started only after `POST /train` returned — large embeddings JSON could stall the POST; (2) `refreshSaeStatusUi` nulled status on fetch error → fallback label "Starting…"; (3) async `setInterval` overlap. Fix: poll immediately on busy; keep last status on error; fetch timeouts; ignore pre-POST `idle`; clear poll in `_stopSaeTrainBusy`. + +### 4.11. COMPARE group contrast visibility (v2.1.0) +- **SAE + global z-score**: Top‑K densified matrix (~5% nonzero) → exact zeros normalize to \(t \approx -0.24\) (false negative “dust”). Mitigation in v1: auto **+ Only** on SAE encode success; restore previous filter on SAE OFF (`saeFilterBridge`). Do not change global z-score contract without an explicit D10. +- **Amplitude floor**: `COMPARE|ANALYSIS|POINTS` at Amp `1.0` collapses relief even when math has signal — default Amp **16** (RIBBONS twin too). +- **Dim sort**: optional client permutation by max pairwise `|mean_Gi − mean_Gj|`; session-only, OFF default; only when ≥2 `groupId`. Does not mutate backend payload. +- **Cosine ▲/▼ vs groups**: disable while groups active — global cosine sort breaks GROUP_* contiguity and soft Y gaps. +- Soft ANALYSIS Y gap: +1 empty slot between consecutive different `groupId` blocks (`groupStackLayout`). + +#### Handoff for predecessor / sister apps (architect brief) +Portable findings from VHectorLab 3D `v2.1.0` — apply if the older app shares ANALYSIS matrix paint + groups + SAE Top‑K: + +| Finding | Detail | Recommended fix | +| :--- | :--- | :--- | +| Sparse SAE + **global** z-score+tanh | Exact zeros → \(t\approx-0.24\) (painted as negative “dust”); nonzeros saturate near +1 | Prefer **+ Only** when SAE ON; optionally restore prior filter on SAE OFF. Avoid changing global norm unless product accepts colormap shift | +| Flat relief | Amplitude default at slider floor (~1) hides real peaks (RAW peak \|a\|~0.22 → Y tiny) | Raise ANALYSIS Amplitude default (VL3D used **16**; ARITH often **40**) | +| Unsorted X | Dim index order hides domain bands even when between-group signal exists | Client **dim permutation** by `max_{i,j} \|mean_Gi − mean_Gj\|`; toggle OFF = raw order | +| Cosine list sort vs groups | Global cosine reorder breaks group contiguity on Y | Disable cosine ▲/▼ while ≥2 groups, or regroup after sort | +| Expectation vs math | RAW G1↔G2 centroid cosine ~0.55 (not antipodal); SAE can improve (~0.22) but paint still hides it | Fix paint/order first; wider SAE train is L3, not required for readability | +| Layout axes (ANALYSIS) | X=dim, Y=thread stack + val×amp, Z=0 | Soft Y gap between `GROUP_*` blocks (+1× spacing) aids domain reading | + +### 4.11b. Group contrast paint (shared noise / sign conflict) +- **Problema**: En Compare, dims “chatas” compartidas por todos los tokens son ruido común; dims G1↔G2 de signo opuesto son señal — sin controles de paint dedicados. +- **Solución**: Deep module `groupDimContrast.js` — **Shared noise** = min/max crudo sobre **todos** los embeddings del batch (`groupId` ignored; gate ≥2 tokens); `sim = 1−|Δ|/(|a|+|b|)`; cancel ZC-style. **Sign conflict** = means G1↔G2; highlight + conflict cover. Solo paint (Y intacto). UI: Shared noise junto a Zero coverage; Group contrast = Sign conflict + Group hue (≥2 grupos). Coverage 30…100% vía knobs A/mA (`coverageAmKnobs.js`). +- **Invariante**: Zero coverage y Shared noise independientes; On habilita knobs (Off = grisado); shader POINTS usa `aCancel`/`aHighlight` + `uColorHighlight`; métrica Shared noise = valores **crudos** del embedding visualizado (no t normalizado). + +### 4.11c. Group hue (per-group black → color) +- **Problema**: Rampa divergente global no distingue dominios `GROUP_*` en Galaxy/Compare. +- **Solución**: Toggle `groupHueEnabled` + map `groupHueColors`; base = lerp(black, groupHex, remapped t) when ON (`groupHuePaint.js`); POINTS via `aBaseColor` + `uUseGroupHueBase`; RIBBONS via CPU `colorForActivationWithGroupHue`. Zero coverage remaps \|t\| then black↔color. Missing `groupId` → divergent fallback. +- **Invariante**: Shared noise / Sign conflict still apply after base; default OFF; gate ≥2 groups; persist under `vl3d.viz.*`. +- **UI labels**: filas Group hue usan `groupsForHueUi` → `{ id, label }` con `groupLabel` del textarea (mismo nombre que badges 3D / chips). No reutilizar el grid de Colors (`28px` para `+1`/`0`/`−1`); layout propio ~3 filas visibles + `overflow-y: auto` si hay 4+. + +### 4.12. Bottom HUD hover activation (POINTS / RIBBONS) +- **Problema**: `ACTIVATION` siempre `0.0000` — `Interaction` pasaba `userData` del mesh (`{ pointsData }`), y el HUD leía `userData.val` / `data.activation` (inexistentes). +- **Solución Obligatoria**: Resolver vía `resolveHoverTelemetry` → `pointsData[index].activation` (POINTS) o `userData.activations` + face/`index` (RIBBONS / continuity). Formatear con `formatActivationValue` (hasta 32 decimales, shrink + scientific si el slot es angosto; label `ACT:` en mobile). `title` tooltip con valor completo. +- **Invariante**: telemetría de hover ≠ normalización de color; mostrar valor **crudo** del embedding/activación en ese índice. + +--- + +### 4.13. Field-info tips ("i") — tap, not hover-only +- **Problema**: Native `title=` tooltips fail on phone (long-press unreliable); editable fields had almost no per-control help. +- **Solución Obligatoria**: Deep module `fieldInfo.js` — `FIELD_INFO` short EN catalog (≤28 chars), `infoTipMarkup`, `wireFieldInfo` (one tip at a time, outside/Escape closes, viewport-clamped). Wire on Arithmetic / Compare / SAE params / Spatial sliders / Visualization. Touch: `.field-info-btn` / `.field-info-tip` in `isUiTouchTarget`. +- **Invariante**: help copy = English, short, mobile-first; do not rely on hover-only `title` for field meaning. + +### 4.14. GUI & Art epic (post-2.2.0) +- **Norte**: canvas 3D = héroe; chrome glass fino; EN copy; portrait-first; no landscape-gate; tips tap (§4.13). +- **Roadmap**: `roadmap/gui-art.md` + prompt `roadmap/PROMPT-gui-art.md` — cerrar A1–A6 (tipo, glass, emoji, rampa, motion, scope) **antes** de codear. +- **Invariante**: polish visual no puede romper §4.1 Top-10, §4.1b mobile MQ, §3.4 startup ANALYSIS, ni WebGL §1. + +--- + +## 5. Protocolo de Mantenimiento de Lecciones Aprendidas + +1. **Consulta Obligatoria**: El agente **DEBE** leer este archivo al iniciar cualquier tarea de implementación, diseño de shaders, navegación o refactorización. +2. **Actualización Continua**: Al descubrir una nueva invariante técnica, bug de renderizado o patrón de rendimiento, el agente **DEBE** agregarla a este archivo antes de finalizar la tarea. + +--- + +## 6. Dev tooling / ngrok / Vite ↔ backend + +### 6.0e. Workbench theme is the default UI (2.4.1+) — parallel skins retired +- **Regla**: el chrome Magic Workbench es la UI **default** en `/` (`src/theme/` + `body.workbench-theme`). Product name = **VHectorLab 3D** (sin la palabra “Amiga” en marca). +- **Retirado en 2.4.1**: MPA `/v25/`, `/amiga/`, árboles `src/v25/**`, `v25/**`, `amiga/**`, `src/amiga/**`. No reintroducir skins paralelos sin pedido explícito. +- **Layout**: fullscreen `#app` + navbar/docks/HUD flotantes. **PROHIBIDO** shell multi-zona left|canvas|right (patrón v25 histórico). +- **Colores**: `VITE_AMIGA_*` en `.env` (nombre histórico de vars; documentadas como Workbench theme). +- **Vite**: `getViteInputs` → solo `main` (`index.html`). Redirects MPA bare-dir vacíos por default. +- **Lección**: no volver a forkear UI en `/v25` o `/amiga` “por si acaso”; un solo producto en `/`. + +### 6.0d. _(histórico)_ `src/v25/**` — retirado +- El árbol v25 fue **eliminado** en 2.4.1. Lecciones 6.0b/6.0c abajo quedan como memoria de bugs de ese experimento; no aplican a código vivo. + +### 6.0c. _(histórico v25)_ MODE switch = visibility toggle, never wipe left host +- **Problema**: montar Compare con `zones.left.innerHTML = …` destruye el panel Arithmetic (form state + Top-10). Además `display: flex` en `.lab-left-host > [data-panel]` **pisa** el UA `[hidden]{display:none}` → el slot Arithmetic oculto sigue ocupando flex y deja un **hueco negro** arriba del Compare (lista cosine aplastada abajo). +- **Solución Obligatoria**: host `.lab-left-host` con dos slots; ambos paneles montados; MODE alterna `hidden` + `.is-hidden` **y** CSS `display: none !important` en `[data-panel][hidden]`. Canvas guarda `lastArithmetic` **y** `lastCompare`. Al entrar COMPARE, VIEW preferido = **NAVIGATION** (framing multi-thread); ARITHMETIC vuelve a **ANALYSIS**. +- **Invariante**: collapse/MODE ≠ unmount; nunca asumir que `[hidden]` gana a una regla `display:` propia. + +### 6.0b. _(histórico v25)_ canvas host size = container, not `window` +- **Problema**: `SceneSetup` legado dimensiona el renderer con `window.innerWidth/Height` (fullscreen `#app`). En un host celda de grid → buffer/aspect incorrectos. +- **Solución Obligatoria**: override `onWindowResize` → `clientWidth/Height` del host; `ResizeObserver`; ThreadLabels al tamaño del host. +- **Invariante actual**: el producto default usa fullscreen `#app` (window size) otra vez; no reintroducir grid de zonas sin necesidad. + +### 6.0. Vite entry + FastAPI static +- **Estado 2.4.1+**: un solo `index.html` en la raíz. `getViteInputs` → `{ main }`. FastAPI `resolve_dist_file` sigue sirviendo `path/index.html` si algún día hay subpaths. +- **No hacer**: contaminar el producto con MPA skins paralelos sin OK humano. +- **URL de prueba**: `http://127.0.0.1:5173/` + +### 6.1. Proxy `/api` en Vite: prefijo general, no ruta-a-ruta +- **Problema**: Exponer frontend y backend con **el mismo** hostname ngrok (dos agentes → `:5173` y `:8000`) se pisa; el celu no puede llamar a `127.0.0.1:8000`. +- **Solución Obligatoria**: + - Un solo túnel ngrok → Vite (`:5173`). + - En `vite.config.js`, proxy de prefijo: `'/api' → http://127.0.0.1:8000` (el backend ya monta el router con `prefix="/api"`). + - `VITE_API_BASE_URL=/api` en `.env` (habilitado por defecto en dev/ngrok). `RemoteProvider` usa esa base; header `ngrok-skip-browser-warning` cuando el host es ngrok. +- **¿Hay que mapear endpoint por endpoint?** **No**, si todo el API vive bajo el mismo prefijo (`/api/health`, `/api/arithmetic`, `/api/compare`, …). Un solo `proxy['/api']` cubre rutas nuevas automáticamente. +- **Cuándo sí ruta-a-ruta**: solo si exponés paths **fuera** de `/api` (p.ej. `/health` bare sin prefijo) y querés proxearlos — ahí cada path top-level necesita su propia entrada en `server.proxy`, o movés el contrato a `/api/*`. +- **Invariante**: nuevas rutas backend bajo `/api` → cero cambio en Vite; si alguien agrega un mount root-level, o lo mete bajo `/api` o agrega proxy explícito + lesson. + +--- + +## 7. Product versioning (SemVer) — VHectorLab 3D + +### 7.0. Product name (2026-08-03) +- Canonical product name: **VHectorLab 3D** (was VectorLab 3D / VECTORLAB). Keep `roadmap/` historical docs as-is. +- Do **not** rename `localStorage` keys `vl3d.*` — technical prefix, not brand; renaming breaks persisted prefs. +- Sync brand + SemVer: `manifest.json`, `package.json`, Navbar `version-tag`, FastAPI `app.version`, `CHANGELOG`. v25 retirado en 2.4.1 — no hay `src/v25/version.js`. + +### 7.1. Diagnosis (why it felt broken) +- **Too fast (Aug 2026 epic day)**: `1.1.0`→`1.5.0` burned a **MINOR per roadmap etapa** (docks / landscape / touch / MESH / RIBBONS) on the same calendar day. Semantically each etapa *was* a capability, but the Navbar tag looked like five releases without five ship moments. +- **Too slow (post-`1.5.0`)**: many user-visible changes (defaults, fog, EN copy, slider ranges) sat only under `[Unreleased]` while `manifest` / Navbar stayed `1.5.0`. The tag stopped tracking “what users can do now.” + +### 7.2. Policy (what to bump when) +Sync **`manifest.json` + `package.json` + Navbar `version-tag` + `CHANGELOG` section** together. + +| Bump | When | +| :--- | :--- | +| **MAJOR (`x.0.0`)** | Breaking backend/API contracts, embedding dim, or data shapes that break clients — **or** an explicit product milestone reset when the human asks to restart the line (e.g. **3.0.0** coverage-knobs ship after a long tag lag). | +| **MINOR (`1.y.0`)** | Add **or remove** a product surface: MODE / VIEW / RENDER mode, major panel capability, or comparable user-facing feature. | +| **PATCH (`1.y.z`)** | Fixes, spatial/camera defaults, fog/copy/i18n polish, docs-only sync that still ships. Prefer PATCH over leaving long Unreleased tails. | + +**Cadence rule**: bump **once per shippable delivery** of a notable change — not once per internal etapa on the same day, and not “never until the next epic.” Batch same-day etapas into one MINOR if they ship together. Do **not** leave the Navbar stuck for weeks while Unreleased grows. + +### 7.3. Build numbers — analysis (do **not** use as product version) +Options considered: `1.5.0+42`, `1.5.0.42`, CI build id in the Navbar. + +| Approach | Pros | Cons for this repo | +| :--- | :--- | :--- | +| **SemVer build metadata (`1.6.0+gitsha`)** | Traceable artifacts | Not for “is this a new feature?”; Navbar noise; easy to confuse with PATCH | +| **Four-part `1.6.0.N`** | Monotonic every merge | Not npm/SemVer; invents a fourth digit users don’t understand | +| **CI build only in debug HUD** | Debug without product inflation | Fine as *optional* `VITE_SHOW_BUILD` — not the public version | + +**Decision**: product version stays **three-part SemVer**. History of capability = `CHANGELOG`. Optional CI/git SHA belongs in a debug overlay (like cam pose), **not** in the brand version tag. Build numbers do **not** replace PATCH/MINOR discipline. + +--- + +## 8. Local onboarding / `setup.sh` (macOS) + +### 8.1. README must describe the control panel, not a backend-only path +- **Problema**: Un README que solo documenta `cd backend && uv sync && uv run …` deja a juniors sin el flujo real (`./setup.sh` opción 1: deps, tests, backend+frontend, browser). +- **Solución Obligatoria**: README en **inglés**, centrado en `./setup.sh` opción **1** como camino recomendado. Documentar qué hace cada opción del menú. + +### 8.2. Prerequisites: check **and install** on macOS (do not only fail) +- **Problema**: “Verificar que estén instalados” y abortar con “instalalo vos” frena onboarding; el panel debe cerrar el gap. +- **Solución Obligatoria** en `setup.sh` (opción 1 / `ensure_prerequisites`): + - Si falta **`uv`** → instalar con el installer oficial Astral; refrescar `PATH` (`~/.local/bin`, Homebrew). + - Si falta **Node/`npm`** → asegurar Homebrew y `brew install node`. + - Si falta **`.env`** → copiar desde `.env.example`. + - Sync backend: `uv sync --extra dev`; frontend: `npm install` si no hay `node_modules`. +- **Invariante**: en Darwin, option 1 no debe pedir instalación manual de `uv`/Node salvo fallo de red/permisos. + +### 8.3. Platform scope is macOS-only until explicitly ported +- **Problema**: Scripts con `open`, Homebrew paths y bash asumen Mac; documentar “multiplataforma” sin pruebas miente. +- **Hecho / soportado**: creado y probado en **macOS 26.5.1 (Darwin 25.5.0, arm64)**. +- **No preparado**: Windows ni Linux (aunque con pocos ajustes de package manager / PATH / process control suele ser viable). +- **Invariante**: README + banner de `setup.sh` deben declarar esa versión de SO y el estado unsupported; auto-install de Node vía Brew es **macOS-only**. En no-Darwin: warn claro; no fingir soporte. + +### 8.4. Docker Desktop is optional (HF image only) +- **Problema**: Juniors asumen que hace falta Docker Desktop para correr la app; o al revés, option 7 falla sin explicación. +- **Hecho**: el flujo diario (opción **1**) es bare-metal (`uv` + Vite) — **no** requiere Docker. +- **Cuándo sí**: opción **7** (`docker build` de la imagen HF Spaces, torch CPU + vocab NPZ). Opción **8** publica Space Docker cpu-basic vía `hf repos create` + `git push` — **no** necesita Docker Desktop local (el Hub buildea). +- **Solución Obligatoria**: README marca Docker como **optional**; menú etiqueta option 7 con Docker Desktop; `ensure_docker` antes del build; option 8 usa `ensure_hf_cli` + `hf auth`. +- **Invariante**: no meter Docker en `ensure_prerequisites` de option 1. + +### 8.5. HF Space cpu-basic packaging +- **Problema**: `uv sync` en Linux tira wheels NVIDIA; encode de ~10k vocab en cada cold start OOMea o tarda demasiado. +- **Problema 2**: HF exige YAML frontmatter en el `README.md` del Space (`sdk: docker`, `app_port: 7860`); ensuciar el README de GitHub rompe el contrato “README estándar de producto”. +- **Problema 3**: `UV_TORCH_BACKEND=cpu` **no** aplica a `uv sync` (solo a `uv pip`) — el lock PyPI Linux seguía jalando `nvidia-*` multi-GB. +- **Problema 4**: push al Space rechaza binarios no-Xet (p.ej. `demo/vhectorlab-gui-tour.gif`). Sacar el path del tip **no alcanza** si el commit tiene `-p HEAD`: el pack manda historial GitHub donde el GIF sigue viviendo; el hook de HF escanea el pack entero. Además: si `./setup.sh` quedó abierto desde antes del fix, la función vieja sigue **en memoria** — hay que salir (0) y re-lanzar. +- **Solución Obligatoria**: Linux torch vía `[tool.uv.sources]` → index `pytorch-cpu` en `backend/pyproject.toml` + `uv.lock`; Dockerfile `uv sync --frozen` (sin confiar en `UV_TORCH_BACKEND`); precompute `public/vocab_embeddings.npz` en build; `.dockerignore` incluye `demo/`; option 8 compone README, strippea `demo/*` del index y pushea un tip **huérfano** (`commit-tree` **sin** `-p HEAD`) + `--force`; **guards** pre-push (`parents=0` + no `demo/`/gif reachable); contrato Space en **`deploy/hf/space-frontmatter.yml`**. +- **Runtime device**: `/health.device` + navbar `ONLINE (model · cpu|cuda|mps)`. +- **ARITHMETIC persist**: por visitante en `localStorage` (`vl3d.arithmetic.*`) — no disco del Space. +- **Invariante**: no asumir GPU en Spaces Docker; ZeroGPU no aplica a sdk docker. **No** force-pushear `HEAD` crudo al Space si el README de producto no lleva frontmatter — siempre inyectar desde `deploy/hf/`. Option 7 necesita disco libre local (BuildKit I/O); option 8 no necesita Docker Desktop. +- **Space model (2026-08)**: cpu-basic aguanta Arctic-m (~1.2 GB) + truncate 256 + vocab EN∪ES; alinear Dockerfile a `MODEL_PROFILE=local-full` para que Shared noise coincida con el lab Mac. No volver a pinear `all-mpnet` en el Space sin OK explícito. + +### 8.5. Local folder / clone name = `vhectorlab` +- **Problema**: el working copy histórico se llamaba `lsv2`, luego `VHectorLab-3D`, mientras el remoto GitHub pasó a **`vhectorlab`**. +- **Solución Obligatoria**: carpeta local canónica **`vhectorlab`** (minúsculas, alineada al repo). README: `git clone …/vhectorlab.git` + `cd vhectorlab`. Producto UI sigue **VHectorLab 3D**. No renombrar `vl3d.*` localStorage ni packages npm (`vhectorlab-3d`) solo por el folder/repo slug. +- **Invariante**: `roadmap/` puede seguir mencionando `lsv2` / VectorLab / `VHectorLab-3D` como historia; no reescribir archivo salvo necesidad. + +### 8.6. Renaming the repo folder breaks `backend/.venv` shebangs +- **Problema**: Tras `mv lsv2 → VHectorLab-3D`, `uv sync` reporta OK pero `uv run pytest` falla: `Failed to spawn: pytest` / `No such file or directory`. Los entrypoints en `.venv/bin/*` siguen con shebang absoluto a la ruta vieja (`…/lsv2/backend/.venv/bin/python3`). +- **Solución Obligatoria**: + 1. En `setup.sh` `ensure_project_deps`: si `backend/.venv/bin/pytest` existe y su intérprete shebang **no existe**, `rm -rf backend/.venv` y re-sync. + 2. Invocar tests como `uv run python -m pytest` (menos frágil que el script `pytest` con shebang roto). + 3. Recuperación manual: `rm -rf backend/.venv && cd backend && uv sync --extra dev`. +- **Invariante**: un rename/move del working copy **implica** recrear el venv; `uv sync` solo no reescribe shebangs rotos. + +### 8.7. Idempotent start — never bounce a healthy stack +- **Problema**: Opción 1 hacía `pkill` + relaunch siempre, aunque backend/frontend ya estuvieran healthy → pérdida de estado, re-carga del modelo, downtime innecesario. +- **Problema 2 (Ctrl+C / PGID)**: sin sesión nueva, backend/vite compartían señales con el panel → SIGINT los mataba. +- **Problema 3 (`set -m` / STAT T)**: `set -m` + job control dejó Vite en **stopped** (`STAT T`): puerto en LISTEN pero HTTP timeout → probe **sick** y el usuario quedaba trabado si option 1 solo abortaba. +- **Solución Obligatoria** en `setup.sh` (opciones que arrancan servicios): + - Arrancar con **`launch_detached`** (`python3` + `start_new_session=True`, stdin DEVNULL) — sin controlling TTY; **no** usar `set -m` para esto. + - **healthy** = proceso matching (`pgrep`) **y** health OK (`/health` con `"status":"ok"` en `:8000`; HTTP OK en `:5173`). Puerto ocupado sin ese combo = **sick**. + - Ambos healthy → **skip** prerequisites, **skip** tests y **skip** start; abrir browser. Tests explícitos = opciones **4/5**. + - **Sick** o parcial → **recycle** ambos (kill + start), no abortar dejando al usuario trabado. + - Ambos down → flujo normal (prereqs → tests → start). + - Opción **2** (backend only): skip si healthy; recycle si sick. + - Opción **10** siempre hace stop real (`kill_stack`), no no-op. + - **Ctrl+C** nunca baja servicios: en live logs pausa → Enter vuelve al menú; segundo Ctrl+C sale del panel. +- **Invariante**: no hay flag “force restart” separado; el stop explícito basta. No bajar+subir un stack ya healthy. Solo opción **10** detiene el stack a propósito. Servicios NUNCA comparten sesión/TTY con el panel. + +### 8.8. Arctic / GTE remote code on transformers≥5 (no xformers) +- **Problema**: `Snowflake/snowflake-arctic-embed-m-v2.0` (profile `local-full`) falla al swap/precompute con `AssertionError: please install xformers` porque el Hub config trae `use_memory_efficient_attention=true`. En macOS/MPS xformers suele no estar / no servir. +- **Problema 2**: aunque se desactive MEA, transformers **5.x** materializa en meta device y **corrompe** buffers `persistent=False` del remote GTE (`position_ids`, RoPE `inv_freq`/`cos_cached`/`sin_cached`) → `IndexError` o embeddings NaN. El remote code no los re-inicia en `_init_weights` (HF #43644 / #43950). +- **Solución Obligatoria** en `backend.model_catalog.build_model` cuando `trust_remote_code`: + 1. `config_kwargs={use_memory_efficient_attention: False, unpad_inputs: False}` (sin xformers; unpad sin MEA rompe RoPE). + 2. Post-load `_repair_gte_nonpersistent_buffers`: rearmar `position_ids` + `rotary_emb` (`inv_freq` + `_set_cos_sin_cache`). +- **Invariante**: no agregar `xformers` como dep de lab/macOS; no asumir que el remote GTE es compatible con transformers 5 sin repair. + +### 8.9. Shared noise visibility depends on embedding geometry (not model-specific wiring) +- **Problema**: Shared noise “funcionaba” en el lab Mac (`local-full` Arctic @256) y en HF con `all-mpnet-base-v2` @768 el knob **no movía nada**. Primera lectura: bug de deploy / cableado distinto por modelo. +- **Hecho**: el path JS (`groupDimContrast.js`) es **agnóstico al modelo** — solo min/max + sameSign + similarity sobre floats. No hay `if (model === …)`. +- **Causa real**: veto D4 (un token con signo opuesto anula la dim) + batch diverso ⇒ en mpnet @768 casi no quedan dims same-sign (medido ~7% / ~3% con cancel>0 @77% Similarity); en Arctic @256 el mismo texto deja ~30% same-sign / ~21% cancel>0 → se ve. Densidad de paint ≈ fracción de dims, no conteo absoluto. +- **Solución Obligatoria**: + 1. No “arreglar” Shared noise especializando por Hub id sin decisión de producto. + 2. Para demos alineadas al lab: Space = `MODEL_PROFILE=local-full` (Dockerfile). + 3. Ante knob “muerto”: medir `sameSign%` / cancel density del batch **antes** de tocar shaders. + 4. Estudios empíricos de este tipo viven en **`current-research/`** (no en `lessons-learned` ni en `roadmap/` como si fueran tickets). Evidencia completa: `current-research/DISCOVERY-shared-noise-embedding-geometry.md`. +- **Invariante**: lección de ingeniería acá; ciencia abierta / ablaciones / rediseño de métrica → `current-research/` + handoff omit-common. SAE ON no es el instrumento para cazar “pack compartido” en RAW. diff --git a/.agents/skills/dev-protocol/qa-review.md b/.agents/skills/dev-protocol/qa-review.md new file mode 100644 index 0000000000000000000000000000000000000000..df614a61b3a6d7882ada715a4c7d37c2eca6d901 --- /dev/null +++ b/.agents/skills/dev-protocol/qa-review.md @@ -0,0 +1,41 @@ +# QUALITY ASSURANCE & SYSTEM REVIEW WORKFLOWS + +Follow these protocols to maintain spec compliance, code quality, and clear, durable issue reporting. + +--- + +## 1. QUALITY ASSURANCE & ISSUE FILING + +When the user describes problems during QA, or when planning a feature, translate them into durable, clear issues on the project issue tracker. + +### 1.1. Rules for Filing Issues +- **Durable**: Issues should remain valid after major refactors. +- **Describe behaviors, not code**: Say *"the login page fails to redirect after submitting"* instead of *"authController.js throws on line 42"*. +- **No file paths or line numbers**: These go stale rapidly. +- **Use the project's domain language**: Reference terms as defined in `architecture_spec.md`. +- **Reproduction steps are mandatory**: List concrete, numbered steps a developer can follow, including inputs, flags, or configuration. + +### 1.2. Single Issue vs Breakdown +- **Single Issue**: Use when it is a single behavior wrong in one place, or multiple symptoms caused by the exact same root behavior. +- **Breakdown**: Break a task or report into multiple issues when: + - The work spans multiple independent areas. + - Slices are independently fixable and verifiable. + - There are blocker/dependency relationships (e.g. Issue B cannot be tested until Issue A is completed). +- **Tracer Bullet Breakdown**: When breaking down a plan, create **vertical slices** (tracer bullets) that cut through all layers (schema -> API -> UI -> tests) and are demoable. Publish issues in dependency order (blockers first). + +--- + +## 2. TWO-AXIS REVIEW WORKFLOW + +To verify changes before merging, perform a review of the diff against `HEAD` along two distinct, independent axes: + +### 2.1. The Two Review Axes +1. **Standards Axis**: Checks if the diff conforms to this repository's documented coding standards (e.g., `CODING_STANDARDS.md`, formatting rules, type structures). +2. **Spec Axis**: Checks if the diff faithfully implements the originating issue, spec, or PRD. + - Detects missing/partial requirements. + - Detects scope creep (behavior in the diff that wasn't asked for). + - Detects incorrect implementations of requirements. + +### 2.2. Parallel Evaluation Protocol +- **Parallel Sub-Agents**: Run the Standards review and Spec review as two parallel sub-agents (using `general-purpose` sub-agents) to prevent context pollution. +- **Integration**: Aggregate both reports under `## Standards` and `## Spec` headers verbatim. Do not merge or rerank their findings, as a change can pass one axis while failing the other (e.g., standard-compliant code that implements the wrong feature). diff --git a/.agents/skills/dev-protocol/templates/.env.example b/.agents/skills/dev-protocol/templates/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..fb6eec15b64bd6ff0ab21fc9eb5ddd5b95531e14 --- /dev/null +++ b/.agents/skills/dev-protocol/templates/.env.example @@ -0,0 +1,44 @@ +# ============================================================================= +# DEV AGENT PROTOCOL — environment template for Python LLM apps +# ----------------------------------------------------------------------------- +# Usage in a new app: +# 1. Copy this file to the workspace ROOT as `.env.example` and commit it. +# 2. Copy it again to `.env`, fill in real values, and KEEP `.env` GITIGNORED. +# +# Rules (see dev-protocol/SKILL.md §3.2): +# - Secrets (keys/tokens) live here, never in code, never committed. +# - `.env.example` documents the variables WITHOUT real values. +# - Model / provider / endpoint are configuration, not hardcoded constants. +# +# Document only the variables your app actually reads; delete the rest. +# ============================================================================= + +# --- Provider selection ------------------------------------------------------ +# Which adapter to use behind the single provider interface. +# Examples: "local" | "remote" (or a concrete backend id your app supports) +LLM_PROVIDER=local + +# --- Remote provider (hosted API) -------------------------------------------- +# Leave blank in the example; fill real values only in your local .env. +LLM_API_KEY= +LLM_BASE_URL=https://api.example.com/v1 +LLM_MODEL= + +# --- Local provider (e.g. Ollama / llama.cpp / vLLM) ------------------------- +# Endpoint of the locally running model server. +LOCAL_LLM_BASE_URL=http://localhost:11434 +LOCAL_LLM_MODEL= + +# --- Generation defaults (configuration, not literals in code) --------------- +LLM_TEMPERATURE=0.0 +LLM_MAX_TOKENS=1024 +LLM_REQUEST_TIMEOUT_SECONDS=60 + +# --- Observability (token / latency / cost logging — SKILL.md §3.2) --------- +# Toggle structured logging of token counts and latency for regression tracking. +LLM_LOG_USAGE=true + +# --- App runtime ------------------------------------------------------------- +LOG_LEVEL=INFO +# HOST=0.0.0.0 +# PORT=8000 diff --git a/.agents/skills/dev-protocol/templates/.pre-commit-config.yaml b/.agents/skills/dev-protocol/templates/.pre-commit-config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..724853a72c914dcff19c6172beaa263af15e5a69 --- /dev/null +++ b/.agents/skills/dev-protocol/templates/.pre-commit-config.yaml @@ -0,0 +1,63 @@ +# ============================================================================= +# DEV AGENT PROTOCOL — pre-commit base template (Python / uv) +# ----------------------------------------------------------------------------- +# Usage in a new app: +# 1. Copy this file to the workspace ROOT as `.pre-commit-config.yaml`. +# 2. Install the git hook once: uv run pre-commit install +# 3. Smoke-test the whole repo: uv run pre-commit run --all-files +# +# Pin the `rev:` tags to the latest releases when you adopt this in a repo, +# then let `pre-commit autoupdate` keep them current. Versions below are +# conservative defaults, not gospel — see dev-protocol/git-workflow.md §2. +# ============================================================================= + +repos: + # --- General hygiene ------------------------------------------------------- + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-added-large-files + args: ["--maxkb=1024"] + - id: check-yaml + - id: check-toml + - id: detect-private-key # blocks committed private keys + + # --- Lint + Format (ruff replaces black + flake8 + isort) ------------------ + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.4 + hooks: + - id: ruff # lint + args: ["--fix"] + - id: ruff-format # format + + # --- Secret scanning (enforces "no keys in git" — SKILL.md §3.2) ----------- + - repo: https://github.com/gitleaks/gitleaks + rev: v8.21.2 + hooks: + - id: gitleaks + + # --- Local guard: never let an actual .env file get staged ----------------- + - repo: local + hooks: + - id: block-dotenv + name: block committing .env files + entry: "Refusing to commit a .env file. Keep secrets out of git; commit .env.example instead." + language: fail + files: '(^|/)\.env(\..+)?$' + # Allow the documented example file through: + exclude: '(^|/)\.env\.example$' + +# --- Optional: project-wide type checking ------------------------------------ +# Type checking the full tree is often too slow for every commit. Prefer +# running it in CI, or enable it here as a manual stage: +# uv run pre-commit run --hook-stage manual mypy +# +# - repo: https://github.com/pre-commit/mirrors-mypy +# rev: v1.13.0 +# hooks: +# - id: mypy +# stages: [manual] +# additional_dependencies: [] # add type stubs your project needs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..f913c8b8950d00f16cc75e39d388c660213a8ca3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,35 @@ +.git +.github +.venv +backend/.venv +node_modules +dist +# GitHub README demo assets — not needed in Space image; HF also rejects non-Xet binaries on git push +demo +**/__pycache__ +**/*.py[cod] +**/.pytest_cache +**/.ruff_cache +**/.mypy_cache +**/.coverage +**/htmlcov +*.log +logs +.env +.env.* +!.env.example +.DS_Store +*.swp +.cursor +.agents +agent-transcripts +roadmap/archivo +CONTEXT.blueprint.md +context.txt +public/vocab_embeddings.npz +backend/artifacts +*.pt +.vscode +.idea +coverage +test-results diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..477f5d273a7c1a369e6944f64d9bffd74f5b4886 --- /dev/null +++ b/.env.example @@ -0,0 +1,55 @@ +# VHectorLab 3D - Environment Variables +HOST=127.0.0.1 +PORT=8000 +MODEL_NAME=all-mpnet-base-v2 +# Default stays EN baseline (all-mpnet). Optional profiles after setup option 11: +# MODEL_PROFILE=local-comfort +# MODEL_NAME=sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 +# TRUNCATE_DIM= +VOCAB_PATH=public/vocab.txt +# EN∪ES merged vocab (after: uv run python scripts/merge_vocab_en_es.py): +# VOCAB_PATH=public/vocab_en_es.txt +# Optional precomputed vocab embeddings (Docker/HF builds this automatically) +VOCAB_EMBEDDINGS_PATH=public/vocab_embeddings.npz +LOG_LEVEL=INFO +# Local bare-metal: unset or 1. Docker/HF Space: 0 +# UVICORN_RELOAD=1 + +# Frontend (Vite) — show live camera POS/ROT overlay for capturing default views +VITE_SHOW_CAM_POSE=false + +# Top-K SAE + embedding runtime device: AUTO | CPU | CUDA | MPS +SAE_DEVICE=AUTO + +# API base for the browser client (RemoteProvider). +# Enabled default for ngrok/phone: same-origin `/api` → Vite proxies to :8000 +# (one public tunnel to :5173 is enough; do not reuse the same ngrok host for :8000). +# +# VITE_API_BASE_URL=/api +# +# Alternatives: +# (unset) auto — localhost → http://127.0.0.1:8000 ; public host → /api +# http://127.0.0.1:8000 +# https://.ngrok-free.dev # dual-tunnel (two different URLs) +VITE_API_BASE_URL=/api + +# --- Workbench theme (MagicWB) — hex #RRGGBB; omit to use defaults --- +# Dark gray surfaces (pens 0/4/5 + bg); light fg for contrast +VITE_AMIGA_PEN_0=#2A2A2A +VITE_AMIGA_PEN_1=#000000 +VITE_AMIGA_PEN_2=#FFFFFF +VITE_AMIGA_PEN_3=#3B67A2 +VITE_AMIGA_PEN_4=#1A1A1A +VITE_AMIGA_PEN_5=#3D3D3D +VITE_AMIGA_PEN_6=#AA907C +VITE_AMIGA_PEN_7=#FFA997 +VITE_AMIGA_BG=#222222 +VITE_AMIGA_FG=#F0F0F0 +VITE_AMIGA_ACCENT=#3B67A2 + +# Hugging Face Space publish (setup.sh option 8) — Enter accepts these defaults +HF_SPACE_ID=hbauzan/llm-semantic-visualizer +# Force-push ephemeral tip → Space main (Space history ≠ GitHub; needed to replace old Space content) +HF_SPACE_FORCE_PUSH=1 +# YAML contract injected into Space README at publish (GitHub README stays without frontmatter) +# HF_SPACE_FRONTMATTER=deploy/hf/space-frontmatter.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000000000000000000000000000000000..dd84ea7824f11be1eeda22377549cbc1aec7f980 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 0000000000000000000000000000000000000000..48d5f81fa422964dd1eea360efdecfc5dc9a6c87 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000000000000000000000000000000000..bbcbbe7d61558adde3cbfd0c7a63a67c27ed6d30 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..aa4cda58a2ff57f82413c60b76b0fe9bc2d3e61c --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Byte-compiled / optimized files +__pycache__/ +*.py[cod] +*$py.class + +# Environments +.venv/ +venv/ +ENV/ +env/ + +# Environment variables +.env +!.env.example + +# Node dependencies & builds +node_modules/ +dist/ +.vite/ +.pnpm-store/ +pnpm-lock.yaml +pnpm-workspace.yaml + +# Pytest / Coverage +.pytest_cache/ +.coverage +htmlcov/ + +# SAE / model weight binaries +*.pt +!backend/artifacts/.gitkeep +public/vocab_embeddings.npz + +# Local AI regeneration blueprint (do not publish) +CONTEXT_HAB.MD + +# Demo GIF capture tooling / scratch (published tour GIF is tracked) +scripts/capture-galaxy-demo.mjs +demo/** +!demo/vhectorlab-gui-tour.gif + +# Logs & temp +*.log +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..d5f9ed29c6b8b3c27c9e98f017e18a3bc5440251 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,363 @@ +# Changelog + +All notable changes to VHectorLab 3D will be documented in this file. + +## [Unreleased] + +### Fixed +- **HF Space publish (option 8)**: ephemeral tip strips `demo/` binaries and is an **orphan** commit (no `-p HEAD`) so the push pack does not include GitHub history that still has the GIF; HF rejects non-Xet binaries in the whole pack. Composed Space README drops local demo media embeds. +- **HF / Docker torch CPU**: Linux installs `torch` from `pytorch-cpu` index via `[tool.uv.sources]` (nvidia-* no longer pulled). `UV_TORCH_BACKEND` alone never affected `uv sync`. + +### Changed +- **HF Space model = local-full**: Docker/Space now runs `Snowflake/snowflake-arctic-embed-m-v2.0` with `TRUNCATE_DIM=256` + `vocab_en_es.txt` (aligned with Mac lab Shared noise). Fits cpu-basic (~1.2 GB weights / 16 GB RAM). +### Added +- **`current-research/`**: home for empirical discovery notes (beyond `lessons-learned`). First note: Shared noise × embedding geometry (Arctic@256 vs mpnet@768). +- **HF Space README inject**: `deploy/hf/space-frontmatter.yml` + `scripts/compose_hf_space_readme.sh`; option 8 pushes an ephemeral tip with `sdk: docker` / `app_port: 7860` without polluting the GitHub product README. +- **Compare group cosine panel**: with ≥2 `GROUP_*`, the lower list shows group-centroid cosine vs the first group in the textarea (mean of L2 unit vectors → re-L2 → dot); flat / single group stays token-vs-first. 3D viewer unchanged. +- **Multi-embedding catalog (local)**: profiles `local-comfort` / `local-full` / `hf-demo`, setup.sh **option 11** swap (stage `.env` → rebuild vocab NPZ → restart), EN∪ES vocab merge, `/health` fields (`model_profile`, `short_label`, `embedding_dim`, `truncate_dim`), navbar ONLINE chip shows profile · label · dim · device. Cross-lingual smoke: `scripts/smoke_crosslingual_cosine.py` (non-blocking; `--strict` optional). Default `.env.example` remains **all-mpnet-base-v2**. HF Space model policy unchanged. +- **Galaxy / boot progress overlay**: full-viewport card over the canvas (`#boot-progress`) so encode→UMAP progress stays visible when the left dock is collapsed (e.g. switching ARITHMETIC → GALAXY on mobile). + +### Changed +- Left Compare/Arithmetic dock: a bit taller (`min(68vh, 600px, …HUD)`) with side scrollbar + `scrollbar-gutter` when content overflows; never taller than the space above the bottom HUD. +- **License**: project relicensed from MIT to **Apache License 2.0** (`LICENSE` + `NOTICE`; README / HF Space card `license: apache-2.0`; `package.json` `license` field). +- Bootstrap / 2 Groups demo labels: `it_core` (IT terms), `vehicles` (auto lexicon), `women` (English women’s names) — replaces `GROUP_it_core` / `GROUP_1` / `GROUP_2`. +- Panel buttons: Visualize Sequence + Clean/Denoise (SAE) slightly larger; all other panel buttons compact at Shared-noise scale (`0.68rem` / weight 600). +- 3D Spatial Controls: denser padding/gaps (Compare-like); dock stays top-right with gizmo stacked under sliders (no empty mid-gap). +- HUD hover: token caption left of `HOVER TELEMETRY` (right TOKEN column removed); grouped Compare points show `group/token`. +- Galaxy default camera pose: POS `25.5, 155.2, 328.9` / ROT `-23.2, 14.0, 0` (captured CAM POSE). +- Startup chrome defaults: **ARITHMETIC | ANALYSIS | POINTS** (desktop + mobile; was COMPARE | GALAXY). + +## [3.0.1] - 2026-08-26 + +### Added +- **Thread lines** toggle (Visualization panel, top): show/hide POINTS continuity lines along each thread; points and magnitude colors stay. Persists `vl3d.viz.threadLinesVisible` (default ON). + +## [3.0.0] - 2026-08-26 + +Milestone cut: start the **3.x** line after shipping the coverage / Shared noise chrome (the tag had lagged the product for too long). + +### Changed +- **Coverage knobs (Zero coverage + Shared noise)**: DAW-style vertical drag (`ns-resize`), long throw, A=100 + mA forces A→99; **mA** fine fraction now **5 decimal places** (`0…0.99999`, step `1e-5`). Room for a future µA knob if needed. +- Builds on **2.4.3** Shared noise token-batch + A/mA Workbench chrome (30%…100%). + +## [2.4.3] - 2026-08-26 + +### Changed +- **Shared noise** retarget: cancel from **token-batch** min/max across all Compare embeddings (`groupId` ignored); Sign conflict stays G1↔G2 means. Control sits after Zero coverage; gate ≥2 tokens. Group contrast = Sign conflict + Group hue (≥2 groups). +- **Zero coverage / Shared noise Similarity**: range **30%…100%**; dual **A / mA** Workbench knobs + editable % readout (one persisted percent). Dual coarse/fine knobs for Zero coverage and Shared noise — yes, it looks a bit like studio gear cosplay on a Workbench panel. Keeping it anyway: one slider fighting the top end was worse UX than a little theatrical. + +## [2.4.2] - 2026-08-08 + +### Added +- **Galaxy feel**: larger UMAP world (`GALAXY_DEFAULT_SCALE` 96, spacing×240) + Galaxy-only flight profile (cruise 56, Shift ×2, softer look); leave Galaxy restores ANALYSIS/NAV speeds. +- **Galaxy soft stars**: circular solid core + soft halo POINTS (ANALYSIS/NAV squares unchanged). +- **Group hue** (Visualization → Group contrast): per-`GROUP_*` black (−1) → picked color (+1); default OFF; transversal COMPARE paint (POINTS + RIBBONS); Shared noise / Sign conflict still layer on top. + +### Changed +- Workbench value fields (`input` / `textarea` / `select`): dark fill `#1a1a1a` instead of white (color/range/checkbox unchanged). + +## [2.4.1] - 2026-08-08 + +### Added +- **Magic Workbench chrome as the default UI** at `/`: Topaz (self-hosted), dark MagicWB surfaces, Workbench bevels, Compare/Arithmetic panel scroll, env-tunable pens via `VITE_AMIGA_*` (`src/theme/`). +- Product stays branded **VHectorLab 3D** (no “Amiga” in the product name). + +### Changed +- **Single frontend entry**: Vite builds only `index.html` → `/`. Parallel MPAs **`/v25/`** and **`/amiga/`** removed (code under `src/v25/**`, `v25/`, `amiga/`, `src/amiga/**` deleted). +- Theme modules live in `src/theme/` (tokens, chrome overrides, Topaz, `magicwbEnvColors`). +- Roadmaps for v25 / amiga MPA / gui-art parallel skin moved to `roadmap/archivo/`. + +### Fixed (during Workbench cutover) +- Right-dock gray slab behind Spatial Controls (dock-body no longer painted). +- Compare token textarea: black field, muted text. +- Compare/Arithmetic left panels: scroll like Visualization (`min(56vh, 520px)`); preset chips wrap. +- Font antialiasing enabled for Workbench chrome. + +## [2.4.0] - 2026-08-08 + +### Added +- _(Superseded by 2.4.1 cutover.)_ Interim `/amiga/` MPA scaffold + `VITE_AMIGA_*` color env — folded into default `/` in 2.4.1. + +## [2.3.0] - 2026-08-08 + +### Added +- **Galaxy VIEW** (in progress): new VIEW tab with UMAP/PCA/t-SNE chips (PCA/t-SNE grayed); entering Galaxy locks MODE=COMPARE + RENDER=POINTS. +- **`POST /project`**: backend UMAP projection of precomputed embeddings (`umap-learn`); pca/tsne → 501. +- **Bootstrap corpus**: `GROUP_it_core` (100 IT tokens) + existing `GROUP_1` / `GROUP_2` demos (REF inside IT core). +- **Galaxy layout**: one point per token from UMAP positions; group badges at centroids; camera frames IT core; no dim-axis ribbons. +- **Galaxy pipeline + progress**: client-driven encode → SAE? → UMAP → build with status text, progress bar, and step **k/n**; Visualize + SAE toggle in Galaxy; reuse `/compare` cache when texts unchanged. + +## [2.2.1] - 2026-08-07 + +### Added +- **`/v25/` MPA scaffold** (Fase 1): `v25/index.html` + `src/v25/main.js` hello shell; Vite multi-page via `getViteInputs()`; FastAPI `resolve_dist_file` serves nested `dist/v25/index.html` (directory index). Legacy `/` unchanged. +- **GUI & Art v25 plan** (`roadmap/gui-art-v25.md` + `PROMPT-gui-art-v25.md`); epic index `gui-art.md` points agents at `/v25/` phases. + +## [2.2.0] - 2026-08-07 + +### Added +- **Field-info tips ("i")** on every editable control (Arithmetic, Compare, SAE params, Spatial sliders, Visualization): short English tap tips, mobile-safe popover (not hover-only `title`). +- **HF Space cpu-basic demo path** (`feat/hf-space-cpu-demo`): Docker torch CPU (`UV_TORCH_BACKEND=cpu`), vocab embeddings NPZ at image build, `/health.device`, navbar `ONLINE (model · device)`, ARITHMETIC `localStorage` persistence (`vl3d.arithmetic.*`), setup option 7 smoke-run + option 8 `hf` create Space (docker / cpu-basic) + push, README Space YAML frontmatter. + +### Changed +- **Repo / local folder slug**: GitHub + working copy **`vhectorlab`** (was `VHectorLab-3D`). Product name **VHectorLab 3D** unchanged; npm/manifest id stays `vhectorlab-3d`. +- **Landscape gate retired**: no more “Better in landscape” cartel — phone portrait is preferred. +- **Arithmetic Top-10**: list scrolls with a **120px floor** (`max(120px, min(…, dvh))`); on short viewports (≤560px) the whole Arithmetic panel scrolls so neighbors stay reachable. Mobile MQ also matches short landscape phones (`max-height: 500px` + `hover: none`) so touch chrome does not drop off. +- **Startup chrome**: default is **ARITHMETIC | ANALYSIS | POINTS** (was NAVIGATION). + +### Fixed +- **`setup.sh` option 8**: defaults from `.env` (`HF_SPACE_ID`, `HF_SPACE_FORCE_PUSH=1`); skip create if Space exists; force-push to Space remote only (not GitHub) so Enter×N publishes over divergent predecessor history. +- **`setup.sh` idempotent start**: options 1/2 probe process + health before launch; skip bounce when already healthy; refuse when sick; restart both only on partial stack. README + lessons §8.7. +- **`setup.sh` Ctrl+C**: pauses log follow / exits panel without stopping services; only option 10 stops. Menu banner OS line removed. +- **`setup.sh` process group**: enable `set -m` so backend/vite are not in the panel PGID (Ctrl+C was killing the stack). Healthy path also skips tests. +- **`setup.sh` detached session**: replace `set -m` with `start_new_session` launch (Vite was freezing at STAT T / sick). Option 1 recycles sick stacks instead of aborting. +- **Offline modal**: no longer hardcodes `127.0.0.1:8000` (works for local setup + HF Space). +- **Uvicorn reload**: off by default when `HOST=0.0.0.0` or `UVICORN_RELOAD=0` (Docker/HF). + +## [2.1.1] - 2026-08-03 + +### Changed +- **Rebrand**: product name **VectorLab 3D** → **VHectorLab 3D** (UI, docs, package manifest). + +### Fixed +- **HUD ACTIVATION stuck at 0.0000**: resolve hover from `pointsData[index]` / ribbon `activations`; adaptive precision up to 32 decimals (shrink / scientific / `ACT:` on narrow mobile slots). + +## [2.1.0] - 2026-08-02 + +### Added +- **COMPARE group contrast visibility** (`feat/compare-group-contrast-viz`): + - Amplitude default **16** for `COMPARE|ANALYSIS|POINTS` and RIBBONS twin (was floor `1.0`). + - SAE ON auto-sets Visualization filter to **+ Only**; SAE OFF restores the previous filter. + - Soft Y gap (+1× Dist Y) between contiguous `GROUP_*` blocks in ANALYSIS. + - Toggle **Sort dims by group contrast** (session-only, OFF default; visible when ≥2 groups) — permutes X by max pairwise `|Δmean|`. + - Cosine ▲/▼ disabled while groups are active (preserves block layout). + +### Changed +- Roadmap decisions D1–D12 closed (pack 1: L1+L2; L3 deferred; SemVer MINOR). + +## [2.0.0] - 2026-08-02 + +### Added +- **Top‑K SAE Clean/Denoise** (`feat/topk-sae-denoise`): trained Sparse Autoencoder (PyTorch), **not** L1 SAE and **not** sinusoidal fake projection. + - Defaults: 768 → 8192 latents (cap), K=32; **train on current Compare/Arithmetic scope** (not full vocab); **ephemeral in-RAM** session model; `suggest_sae_dims` auto-scales for small N; clear on Visualize/Calculate. + - API: `GET /api/sae/status`, `POST /api/sae/train` (embeddings + async poll), `POST /api/sae/encode` (**Top‑K sparse** `indices`/`values` + `ORJSONResponse`; client densifies), `POST /api/sae/clear`. + - UI: Compare-only 50/50 CTA `[ VISUALIZE | Clean/Denoise (SAE) ]`, Train SAE + params, progress, metrics strip. **No SAE in Arithmetic.** + - Semantics: SAE ON **replaces** all Arithmetic/Compare vectors used for 3D + cosine with sparse activations; OFF restores cached raw 768D. Preference in `localStorage` (`vl3d.sae.*`). Scope change invalidates the session SAE. + - Encode I/O: wire format is `[N, K]` indices+values (not dense `[N, hidden]`); model singleton stays in RAM after first `load_model()`. +- **Zero coverage %** (carried onto this branch): Visualization panel slider expands how much of |t| stays at the zero/black color before blending to ±1 (cap 90%); `vl3d.viz.zeroCoverage`. +- **Hide/Show labels** in Visualization panel: toggles floating thread/group badges; persists `vl3d.viz.labelsVisible`. +- **SAE camera framing**: on Clean/Denoise toggle ON, dim-axis pitch scales so sparse features keep ~RAW wall width; camera soft-lerps to content bounds (ANALYSIS front / NAVIGATION angled). OFF restores Length + context pose with lerp. Empty train hyperparams no longer clamp to 32/k=1. + +## [1.8.0] - 2026-08-02 + +### Added +- **Visualization Controls** (`feat/visualization-sign-color-controls`): right-dock panel under 3D Spatial Controls. + - Sign filter: `All | + Only | − Only` on **normalized** activations (ε=0.01); hides opposite sign and near-zero for ± only. + - Applies to POINTS (shader discard) and RIBBONS / continuity lines (index omission) in ARITHMETIC + COMPARE. + - Three hex color anchors (+1 / 0 / −1) replace the fixed mid-stop divergent ramp; defaults `#FFE600` / `#000000` / `#9900E6`. + - POINTS shader uniforms `uColorPos` / `uColorNeg` / `uColorZero`; live update + `localStorage` (`vl3d.viz.*`) + Reset. + - Edge collapse tab (dock-tab affordance) slides the Visualization card to a thin strip; persists `vl3d.viz.panelCollapsed`. + +## [Unreleased] + +### Changed +- **Defaults COMPARE|ANALYSIS|POINTS** (`feat/compare-analysis-points-defaults`): + - Sliders: Spacing `1.45`, Dist Y `1.0`, Amp `1.0`, Length `0.2`, Thickness `0.01`. + - Camera: `POS (-150.3, 0.7, 276.6)` / `ROT (-0.5, 0.9, 0)`. + +### Fixed +- **Defaults COMPARE|NAVIGATION|RIBBONS + fog off for RIBBONS** (`feat/compare-nav-ribbons-defaults-no-fog`): + - Sliders: Spacing `1.55`, Dist Y `10`, Amp `7`, Length `0.057`, Thickness `0.05`. + - Camera: `POS (-575.8, 43.8, 237.9)` / `ROT (-22.4, -35.7, 0)`. + - `setFogForRenderMode('RIBBONS')` clears scene fog; POINTS keeps soft FogExp2. + - Compare+RIBBONS no longer mounts the POINTS cloud (square dots on strips). +- **RIBBONS dark rectangle through translucent strips** (`fix/remove-ribbons-base-plane`): stop mounting `createBasePlaneForThreads` under wide ribbons (Arithmetic + Compare). Factory helpers retained unused. +- **Remove floor GridHelper** (`fix/remove-scene-grid`): no reference grid under the 3D scene (cleaner RIBBONS/COMPARE views). +- **Scene fog too dense for far RIBBONS** (`fix/soften-scene-fog`): `FogExp2` density `0.008` → `0.0008` so ribbons stay readable at COMPARE-scale camera distance; avoids distance “creeping” darkening. POINTS unaffected (custom shader). + +### Changed +- **Spacing (X) range → `[0.4, 2.0]`** (`feat/spacing-range-center-compare`): same track for all MODE|VIEW|RENDER; COMPARE default `0.7` unchanged. Other combo defaults unchanged (Amp/Thickness ranges untouched). +- **Length (Z) range → `[0.001, 0.2]`** step `0.001`, label 3 decimals: never reaches 0; COMPARE `0.1` ≈ mid; global/Analysis `0.2` at max. +- **Defaults COMPARE|NAVIGATION|POINTS** (`feat/compare-nav-points-defaults`): + - Sliders: Spacing `0.7`, Vector Distance `10`, Amplitude `4.9`, Length `0.1`, Thickness `0.01`. + - Camera: `POS (-106.5, 20.4, 390.2)` / `ROT (-3.9, -8.4, 0)` via `cameraViewDefaults.js`. + - Camera poses now resolve per MODE|VIEW|RENDER (VIEW fallbacks + overrides); applied with sliders on context change. + - First COMPARE entry loads full EN auto-manual lexicon (`COMPARE_AUTO_PRESETS.default`), not `sample5`. +- **English-only product UI** (`feat/english-user-facing-copy`): Navbar VIEW/ANALYSIS/NAVIGATION, spatial slider labels, Arithmetic/Compare buttons, Compare empty/sort/copy + EN auto-parts presets, landscape gate. Internal `data-view` / mode keys unchanged. Test titles and in-scope `src/` comments translated to EN; historical CHANGELOG entries left in Spanish. +- **Docs: English-only UI roadmap** — glossary + D6–D8 closed (EN Compare vocab; EN test titles/comments; keep historical CHANGELOG in Spanish). See `roadmap/english-ui-i18n.md` + `roadmap/PROMPT-english-ui.md`. +- **ThreadLabels cortas** (`feat/short-thread-labels` + `fix/compare-labels-full-tokens`): + - Arithmetic 3D: `WORD_A` / `WORD_B` / `WORD_C` / `RES` / `TOP1` (sin badge de tipo). + - Compare 3D: token completo ingresado (sin `TOPn` / sin truncar) — todos los items de la secuencia. +- **Defaults ARITHMETIC|ANALYSIS|POINTS** (`feat/arithmetic-analysis-points-defaults`): + - Sliders: Separación `0.4`, Distancia Y `10`, Amplitud Y `40`, Longitud Z `0.2`, Grosor `0.05` (override en `spatialSliderDefaults.js`). + - Cámara Análisis: `POS (-75.2, -0.8, 62.5)` / `ROT (0, 0, 0)`. + - Al cambiar MODE/VISTA/RENDER se reaplica el preset resuelto + sync de sliders. +- **Grosor Puntos mid → 0.05** (`feat/thickness-mid-0.05`): + - Default/mid `0.05` ∈ `[0.01, 0.09]` step `0.01` (simétrico lineal). +- **Amplitud (Y) max → 40** (`feat/amplitude-y-max-40`): + - Rango `[1.0, 40.0]` step `0.1`; default sigue en `7.0` (asimétrico — sin regresión del punto dulce al load). +- **Control Espacial 3D — dblclick reset** (`feat/spatial-slider-dblclick-reset`): + - Doble clic en un slider restaura solo ese valor al default del contexto MODE/VISTA/RENDER (hoy = global mid; overrides listos en `spatialSliderDefaults.js`). +- **Control Espacial 3D — finer steps** (`feat/finer-spatial-slider-steps`): + - Gradual intermediate values: Separación step `0.05` (2 dec), Distancia/Amplitud Y step `0.1` (1 dec), Longitud/Grosor step `0.01` (2 dec). Min/max/mid unchanged. +- **Control Espacial 3D — ranges re-centrados** (`feat/recenter-spatial-slider-ranges`): + - Defaults (punto dulce) son el mid lineal de cada slider: Separación X `0.4` ∈ `[0.1, 0.7]`, Distancia Y `10` ∈ `[1, 19]`, Amplitud Y `7` ∈ `[1, 13]`, Longitud Z `0.2` ∈ `[0.1, 0.3]`, Grosor `0.10` ∈ `[0.05, 0.15]`. + - Corrige Distancia Y y Grosor pegados al mínimo al load. + +### Added +- **Ngrok / phone dev access** (`feat/ngrok-dev-access`): + - Vite `allowedHosts` + `/api` proxy to `127.0.0.1:8000` (prefix proxy — not per-route). + - `VITE_API_BASE_URL=/api` enabled in `.env` / `.env.example`; `RemoteProvider` honors it. + - Lesson §6.1: new backend routes under `/api` need no Vite remap. + +## [1.7.0] - 2026-08-02 + +### Added +- **COMPARE groups** (`feat/compare-group-labels`): parse `GROUP_name = tokens…` in the textarea; concatenate groups into the sequence; floating `GROUP_*` badges at member centroids. Preset **2 Groups**. Anchor remains global #1; cosine sort is global (may break contiguity). When groups are active, overlay shows group badges only (token cards hidden — still listed in cosine panel); label layer z-index above docks so badges stay visible. + +### Fixed +- **COMPARE group badges never appeared** (`feat/compare-group-labels`): `ComparePanel` → `handleCalculateCompare` callback dropped `tokenMeta`, so `groupId` never reached Instancer/ThreadLabels (token stack stayed, no `GROUP_*`). + +## [1.6.0] - 2026-08-02 + +### Removed +- **RENDER: MESH** (`chore/remove-render-mesh`): surface heightfield mode retired from navbar and runtime. Supported modes: POINTS | RIBBONS. `normalizeRenderMode` maps legacy `"MESH"` → POINTS. Deleted `createSurfaceMesh` / `updateSurfaceMeshPositions` and `tests/MeshSurface.test.js`. + +## [1.5.0] - 2026-08-01 + +### Added +- **RENDER: RIBBONS + base plane (Etapa E)**: + - `MeshFactory.createWideRibbonMesh` / `createBasePlane` — real-width quad strips + translucent ground (no Line linewidth). + - `Instancer` mutually exclusive branch for `RIBBONS` (Arithmetic + Compare); compare reorder updates wide ribbons in-situ. + - Vitest: `tests/MeshRibbons.test.js`. + +## [1.4.0] - 2026-08-01 + +### Added +- **RENDER: MESH surface (Etapa D)**: + - `MeshFactory.createSurfaceMesh` — indexed quad heightfield (threads × dims), divergent colormap. + - `Instancer` branches on `renderMode === 'MESH'` (no Points); Arithmetic + Compare; compare reorder updates surface in-situ. + - Vitest: `tests/MeshSurface.test.js`. + +## [1.3.0] - 2026-08-01 + +### Added +- **Mobile touch navigation (Etapa C)**: + - Virtual joystick (move) + canvas finger-drag look + ▲/▼ (Q/E) via `TouchControls` → `Navigation.setMoveAxes` / `applyLookDelta` / `setVertical`. + - UI touches on docks/HUD/navbar do not steal look; desktop WASDQE path unchanged (`lerp` §3.1). + - Vitest: `tests/TouchControls.test.js`. + +## [1.2.0] - 2026-08-01 + +### Added +- **Responsive phone layout + landscape-first gate (Etapa B)**: + - Soft portrait overlay (`LandscapeGate`) with dismiss → `sessionStorage`; tablet/desktop unaffected; render loop never pauses. + - `@media (max-width: 768px)`: compact navbar, ≥44px targets, ≥16px inputs, safe-area insets, docks as overlay drawers. + - Vitest coverage in `tests/LandscapeGate.test.js`. + +## [1.1.0] - 2026-08-01 + +### Added +- **Collapsible side docks (Etapa A)**: + - `CollapsibleDock` (`src/ui/CollapsibleDock.js`): left/right docks with edge tabs, ~250ms `transform` slide, no DOM unmount, `aria-expanded` on the tab. + - **Left dock**: hosts Arithmetic *or* Compare (MODE). Shared collapsed state across MODE switches (same `localStorage` key `vl3d.dock.left.collapsed`). + - **Right dock**: spatial sliders + `AxisGizmo` (D1). Key `vl3d.dock.right.collapsed`. + - Desktop persists collapsed in `localStorage`; mobile probe (`max-width: 768px`) defaults collapsed and skips persist (D4 hook for Etapa B). + - Bottom telemetry HUD remains always visible (D3). + - Vitest coverage in `tests/CollapsibleDock.test.js`. + +## [1.0.0] - 2026-08-02 + +### Added +- **COMPARE Cosine-vs-Anchor List + 3D Reorder Tween**: + - `/compare` response enriched with `anchor` and per-item `cosine_vs_first` (dot product vs L2-normalized first token). + - `ComparePanel` scrollable similarity list under ACTIVE SEQUENCE METRICS (`SIMILITUD COSENO vs «…»`), REF badge on #1, per-row ▲/▼ reorder, and header ▼/▲ sort (desc/asc by score, REF stays #1) without camera focus. + - Default COMPARE presets use Spanish auto-manual vocabulary (`rueda`, `motor`, `freno`, `volante`, `embrague`, carrocería, fluidos, …); buttons 5/20/50 slice that lexicon. + - In-memory score recompute on reorder (no backend re-call); `Instancer.animateCompareReorder` lerps thread layout slots (~320ms ease-out) while reusing ribbon/points meshes; `ThreadLabels.updateOrigins` follows during the tween. + - Panel scrollbar invariant preserved: scroll only inside `.compare-cosine-list`. +- **Camera Pose Overlay (`VITE_SHOW_CAM_POSE`)**: Optional live `POS`/`ROT` HUD for capturing default navigation views. Gated by Vite env var; default `false` (see `.env.example`). +- **Square / Cube Point GLSL Shader (`src/visualizer/DivergentShading.js`)**: + - Replaced circular disc discard in `divergentFragmentShader` with axis-aligned square box distance calculation (`max(coord.x, coord.y)`). + - Crisp 1-pixel anti-aliased square bounding edge using `smoothstep(0.44, 0.49, maxDist)` for sharp 3D point cloud rendering. +- **Vertical Origin Baseline in Analysis Mode (`src/visualizer/Instancer.js` & `MeshFactory.js`)**: + - Vertical reference baseline mesh connecting thread origins at $X = \text{startX}$ in Analysis mode. + - Rendered with subtle cyan/gold glass opacity (`opacity: 0.6`, `transparent: true`, `frustumCulled: false`). +- **Dual Workspace Mode Selector (`MODE: [ ARITHMETIC | COMPARE ]`)**: + - Navbar top selector buttons `MODE: [ ARITHMETIC | COMPARE ]`. + - `ComparePanel` sidebar component (`src/ui/ComparePanel.js`) supporting sequence inputs of **1 to 1024 tokens**. + - Fast-loading preset buttons (5, 20, 50 tokens). + - Multi-sequence 3D WebGL layout engine in `Instancer.js` (`renderCompareData`). +- **Backend Batch Compare Endpoint (`/compare`)**: + - Pydantic `CompareRequest` accepting 1 to 1024 text/token items. + - Fast batch encoding and L2 normalization in `AppState.perform_compare`. + - Full Pytest coverage in `backend/tests/test_backend.py`. + +### Changed +- **Default Navigation Camera Pose (`src/engine/Navigation.js`)**: Startup `NAVEGACIÓN` view locked to captured corridor pose `POS (-178.3, 13.5, 52.2)` / `ROT (-5.4°, -51.5°, 0°)` with matching spatial slider defaults (Separación `0.4`, Amplitud `7.0`). +- **Top-10 UI Space Optimization (`src/ui/Sidebar.js` & `src/style.css`)**: + - Removed Top-K dropdown selector from sidebar. + - Locked `top_k` parameter to `10` across form inputs and API calls. + - Compact Top-10 rows (tighter padding/gap) so the Vector Arithmetic panel fits without a scrollbar; panel uses `height: fit-content` + `overflow: hidden`. + +## [0.2.0] - 2026-08-02 + +### Added +- **Vista de "Análisis" 3D con Encuadre Frontal y Cartelitos (`src/ui/ThreadLabels.js` & `src/visualizer/LayoutEngine.js`)**: + - Vista por defecto al iniciar la app con cámara frontal (`Z=360`) encuadrando todos los hilos vectoriales de frente sin necesidad de desplazarse. + - Selector en la Navbar superior: `VISTA: [ ANÁLISIS | NAVEGACIÓN ]` junto al selector de RENDER. + - Apilamiento vertical en el eje $Y$ con separación constante para los hilos `word_a`, `word_b`, `word_c` y `res`, manteniendo activaciones $+1$ (hacia arriba) y $-1$ (hacia abajo). + - Sliders de control espacial 3D ampliados (`src/ui/ThreadSliders.js`): **Distancia Vectores (Y)** (distancia entre baselines de los 5 hilos) y **Amplitud (Y)** (escalado de la altura de picos $+1$ y valles $-1$ con rango ampliado de $1.0$ a $240.0$). + - Inclusión del 5.º hilo `#1 COS VECTOR` posicionado debajo de RESULT VECTOR. + - Cartelitos flotantes Glassmorphic vinculados en tiempo real a la proyección 3D del inicio de cada hilo. + - Tests unitarios en `tests/ThreadLabels.test.js` y `tests/LayoutEngine.test.js`. + +### Changed +- **Simplificación de Rampas Cromáticas Divergentes por Activación 3D (`src/visualizer/DivergentShading.js`)**: + - Eliminación de pasos intermedios de colores Rojo (rango positivo) y Verde (rango negativo) para transicionar directamente desde Negro/Transparente. + - Nueva rampa positiva ($0 \rightarrow +1$): Negro (`vec3(0.0)`) $\rightarrow$ Naranja (`#FF8000`) $\rightarrow$ Amarillo Incandescente (`#FFE600`). + - Nueva rampa negativa ($0 \rightarrow -1$): Negro (`vec3(0.0)`) $\rightarrow$ Azul Eléctrico (`#0040FF`) $\rightarrow$ Violeta Neón (`#9900E6`). + - Optimización computacional GPU/GLSL y CPU para $|t| < 0.01$: retorno directo de opacidad mínima ($\alpha \approx 0.05$) evitando operaciones de interpolación no necesarias. + - Cobertura de tests unitarios en `tests/DivergentShading.test.js` actualizada y extendida. + +## [0.1.0] - 2026-08-01 + +### Added +- **Divergent Activation Shading (`roadmap/archivo/Shading Divergente por Activación.md`)**: + - Solid Circular Point GLSL Shader (`src/visualizer/DivergentShading.js`) rendering crisp, solid circular points with 1-pixel anti-aliasing edges instead of blurred halos. + - Dual Multi-Stop Color Ramps (`src/visualizer/DivergentShading.js`): Positive range ($0 \rightarrow +1$) transitions Negro $\rightarrow$ Rojo $\rightarrow$ Naranja $\rightarrow$ Amarillo. Negative range ($0 \rightarrow -1$) transitions Negro $\rightarrow$ Verde $\rightarrow$ Azul $\rightarrow$ Violeta. + - Full integration in `MeshFactory.js` and `Instancer.js` connecting all 3D thread vector points with continuous ribbon lines. + - Integration with `ThreadFactory.js` buffer attributes (`intensity`, `color`) and `frustumCulled = false` invariant. + - Real-time spatial control sliders (`src/ui/ThreadSliders.js`) updated with specified ranges ($X \in [0.1, 10.0]$, $Z \in [0.1, 5.0]$, Grosor Puntos $\in [0.1, 1.0]$ con valor por defecto $0.3$). + - TDD unit test suite (`tests/DivergentShading.test.js`) verifying multi-stop color ramp math. + +- **Thread Geometry & Spatial Sliders 3D (`roadmap/archivo/sliders.md`)**: + - Synthetic 3D vector thread data factory (`src/visualizer/ThreadFactory.js`) generating buffer geometries for 3D lines and point nodes. + - In-situ GPU Float32Array buffer mutator (`updateAllThreadPositions` in `src/visualizer/LayoutEngine.js`) ensuring zero re-creation of geometries and zero memory leaks. + - Interactive spatial control UI panel (`src/ui/ThreadSliders.js`) with real-time 60fps sliders for lateral separation ($X$), longitudinal scale ($Z$), and node thickness. + - Pure layout math functions (`executeLayoutMath`) with Vitest test coverage (`tests/LayoutEngine.test.js`). + - WebGL scene manager (`src/engine/Scene.js`) with perspective camera, lights, and reference grid at $Y=0$. + +- **Backend Core (Phase 1)**: + - FastAPI server with lifespan lazy-loading of SentenceTransformer (`all-mpnet-base-v2`). + - Pre-computed vocabulary embedding matrix in RAM for fast cosine similarity lookup. + - Core API endpoints: `/health`, `/embed`, `/tokenize`, and `/arithmetic` ($A - B + C$). + - Vocabulary generator script `scripts/generate_vocab.py` with custom word count and URL source support. + - Heartbeat test runner `backend/perform_tests.py`. + - Full unit test suite with `pytest`. + +- **WebGL 3D Engine & Shaders (Phase 2)**: + - Fixed WASD camera flight controller (`src/engine/Navigation.js`) with linear velocity interpolation (`lerp`) and input safety (ignoring keys when focused on UI form inputs and clearing inputs on window blur). + - Custom GLSL point shader with glowing incandescent halos and anti-aliased radial smoothing (`src/engine/Shaders.js`). + - Three.js 3D scene orchestrator with dark background (`#050505`), fog, and reference grid (`src/engine/SceneSetup.js`). + - Inertial flight camera controller with WASDQE, mouse drag look, and Shift turbo acceleration (`src/engine/Navigation.js`). + - Raycaster mouse picking for 3D vector points (`src/engine/Interaction.js`). + - Spatial 3D layout mapper for vector dimension coordinates $X, Y, Z$ (`src/visualizer/LayoutEngine.js`). + - GPU instancing and Mesh factory enforcing `frustumCulled = false` invariant (`src/visualizer/MeshFactory.js` & `Instancer.js`). + - Corner 3D orientation axis gizmo (`src/visualizer/AxisGizmo.js`). + - Vitest test suite (`npm test`). + +- **Control Panel & Hugging Face Deployment (Phase 4)**: + - Interactive CLI control panel (`setup.sh`) supporting dev mode, bare-metal server, heartbeat, vitest, pytest, vocabulary management, and HF Spaces deployment. + - Custom vocabulary management allowing custom file loads or generation of N words. + - Monolithic production Dockerfile for Hugging Face Spaces serving FastAPI on port `7860`. + - Static files serving integration in `backend/server.py` for bundled `dist/` production assets. diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000000000000000000000000000000000000..e55bbc51f5264f0c6eddb20430260fa3f913e648 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,106 @@ +# Context & Domain Model Glossary - VHectorLab 3D + +## Ubiquitous Language + +### Vector Arithmetic +Operation calculating a composite semantic vector $V_{res} = V_A - V_B + V_C$ in high-dimensional embedding space. + +### Vocabulary Matrix +Pre-computed L2-normalized array of word embeddings in RAM enabling sub-millisecond similarity search via matrix multiplication. + +### Lazy Loading +Initialization pattern deferring heavy model loading (PyTorch / SentenceTransformer) to the application lifespan startup event, preserving module import speed and clean testability. + +### Halo Shader +WebGL fragment shader rendering glowing incandescence for 3D activation points mapped to vector magnitudes. + +### Thread Geometry +3D vector representations constructed using continuous Three.js `Line` and `Points` buffer geometries to display multi-point activation series over space. + +### In-situ Buffer Mutation +Updating existing GPU `Float32Array` attributes directly and setting `needsUpdate = true`, avoiding geometry re-creation and memory leaks during real-time UI interactions. + +### Compare Sequence +Ordered list of 1–1024 token/text items visualized as parallel 3D threads; list order equals layout `sequenceIndex`. + +### Compare Group +Named block in Compare input (`name = tokens…`) that concatenates members into the sequence and renders a floating group badge at the member-origin centroid (screen-offset left of token labels). Default bootstrap: `it_core` / `vehicles` / `women`. Global cosine sort may interleave groups. + +### Anchor Token +The token currently at position #1 in a Compare Sequence. Cosine similarity for every row is computed against this embedding (`cosine_vs_first`). + +### Cosine-vs-First Score +Dot product between an item’s L2-normalized embedding and the Anchor Token embedding; the anchor row is always `1.0000` (REF). + +### Collapsible Dock +Edge-hosted UI host that slides off-screen via CSS `transform` while keeping children mounted; left dock hosts Arithmetic or Compare, right dock hosts spatial sliders and AxisGizmo. The bottom telemetry HUD is not a dock. + +_Avoid_: treating Sidebar/ComparePanel as independent minimize targets; unmounting panel DOM to “hide” it. + +### Landscape Gate +_Retired._ Former soft portrait overlay that suggested landscape; product prefers phone portrait. Do not reintroduce without an explicit ask. + +### Field Info Tip +Compact tap “i” control next to an editable field; shows a short English popover (mobile-safe, not hover-only `title`). Catalog lives in `fieldInfo.js`. + +### Wide Ribbon +Quad-strip mesh with real lateral width following a thread centerline, colored by activation; used by `RENDER: RIBBONS`. Distinct from 1px WebGL lines. + +### Visualization Controls +Bottom-HUD glass panel for global sign filter, divergent color anchors, Zero Coverage, Shared noise (Compare ≥2 tokens), and Group contrast (Compare ≥2 groups). + +### Sign Filter +Global show mode `all | positive | negative` over **normalized** activations (post z-score/tanh); near-zero `|t| < 0.01` is treated as neutral and hidden by +/− only. + +### Color Anchor +User-editable hex for normalized activations at +1, 0, and −1; replaces the former fixed dual mid-stop ramp via linear RGB lerp. + +### Zero Coverage +Percent of the |t| range held at the zero color (default black) before blending toward ±1 anchors; range 30%…100%, edited via A/mA knobs + readout. + +### Shared Noise +Compare paint that blackens dims where **all tokens in view** agree in sign and magnitude (min/max over embeddings; `groupId` ignored). Sits with Zero coverage; Similarity uses the same A/mA coverage chrome. Default OFF. + +### Group Contrast +Visualization paint for Compare with ≥2 groups: **Sign conflict** highlights opposite-sign dims (G1↔G2 means, custom color × |Δ|) and can blacken them by difference; **Group hue** optional. Geometry Y unchanged. + +### Workbench Theme +Default product chrome at `/`: Magic Workbench–inspired palette, Topaz typography, and bevelled panels on the fullscreen + floating-dock layout. Tunable via `VITE_AMIGA_*` in `.env`. Product name remains **VHectorLab 3D**. Value fields use dark fills (not white). + +### Galaxy feel +Galaxy VIEW uses a larger UMAP world scale and a slower flight profile (WASD/QE + mouse look) than ANALYSIS/NAVIGATION; leaving Galaxy restores default speeds. Galaxy POINTS render as soft circular stars. + +### Group hue +Optional Visualization → Group contrast mode: each `GROUP_*` paints black (−1) → a picked color (+1), coexisting with Shared noise / Sign conflict. Default OFF. + +_Avoid_: parallel MPA skins (`/v25/`, `/amiga/` — retired in 2.4.1); multi-zone lab grids as the primary layout. + +### Shared Noise Similarity +`1 − |mean_G1 − mean_G2| / (|mean_G1| + |mean_G2|)` per dimension — high values mean shared-sign “noise” between groups. + +### Top‑K SAE +Trained sparse autoencoder with exactly K active latents per input (ReLU + Top‑K, no L1 shrinkage). Trained on the **current workspace scope** (Compare/Arithmetic batch); ephemeral in-RAM session model. Default caps 768 → 8192 with K=32 (auto-scaled for small N). + +### Embedding Catalog +Backend single source of truth (`model_catalog`) listing allowed SentenceTransformer Hub IDs plus flags (E5 mode, trust_remote_code, gated, default truncate). + +### Model Profile +Named local preset (`local-comfort`, `local-full`, `hf-demo`) that resolves to a catalog Hub ID and optional `TRUNCATE_DIM`. Selected only from `setup.sh` option 11 — not from the web UI. + +### Active Model +The one SentenceTransformer loaded in the backend process; exposed on `/health` as `model` / `short_label` / `embedding_dim` / `device` (and `model_profile` when set). + +### Truncate Dim +Optional Matryoshka width (`TRUNCATE_DIM`) applied after encode; effective `embedding_dim` is the truncated width when set. + +_Avoid_: generative LLM “embeddings”; multi-model residency; in-app model switcher. HF Space model is pinned in the Dockerfile (currently `local-full` / Arctic @256); changing it needs an explicit approval + republish (option 8). + +### Clean/Denoise (SAE) +Compare-only toggle that replaces raw 768D embeddings with SAE sparse activations for 3D threads and cosine while ON. Requires Train SAE on current Visualize data; scope changes clear the session model. Not available in Arithmetic. + +### SAE Feature Space +Expanded latent dimension (default cap 8192, auto-scaled for small N) used for visualization and metrics while Clean/Denoise is enabled. + +### Dead Features +Latents that never activated on the training set; reported in SAE train metrics. + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..9825fe8f8850276a494d7922da0bdfc3a21e5265 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,66 @@ +# Monolithic Dockerfile for VHectorLab 3D on Hugging Face Spaces (Port 7860) +# Target hardware: cpu-basic (2 vCPU / 16 GB / 50 GB) — Arctic-m ~1.2 GB fits. +# Space profile matches local-full so Shared noise / Compare match the Mac lab. +FROM python:3.10-slim + +# Install system dependencies & Node.js +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + git \ + build-essential \ + && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y nodejs \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Install uv for Python package management +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +# Copy configuration and package files +COPY package.json package-lock.json ./ +COPY backend/pyproject.toml backend/uv.lock backend/README.md ./backend/ + +# CPU-only torch on Linux via [tool.uv.sources] → pytorch-cpu (not UV_TORCH_BACKEND / uv sync). +RUN npm ci +RUN cd backend && uv sync --frozen + +# Copy source code and vocabulary +COPY . . + +# Production frontend bundle (same-origin /api) +ENV VITE_API_BASE_URL=/api +RUN npm run build + +# Match local-full: Arctic-m-v2 Matryoshka @ 256 + EN∪ES vocab (precompute at image build). +ENV MODEL_PROFILE=local-full +ENV MODEL_NAME=Snowflake/snowflake-arctic-embed-m-v2.0 +ENV TRUNCATE_DIM=256 +ENV VOCAB_PATH=public/vocab_en_es.txt +ENV VOCAB_EMBEDDINGS_PATH=public/vocab_embeddings.npz +ENV SAE_DEVICE=CPU +RUN uv run --directory backend --frozen python /app/scripts/precompute_vocab_embeddings.py \ + --device CPU \ + --profile local-full \ + --truncate-dim 256 \ + --vocab /app/public/vocab_en_es.txt \ + --out /app/public/vocab_embeddings.npz \ + --model Snowflake/snowflake-arctic-embed-m-v2.0 + +# Expose Hugging Face Space default port 7860 +EXPOSE 7860 + +# Environment variables for Docker / HF Space mode +ENV HOST=0.0.0.0 +ENV PORT=7860 +ENV UVICORN_RELOAD=0 +ENV MODEL_PROFILE=local-full +ENV MODEL_NAME=Snowflake/snowflake-arctic-embed-m-v2.0 +ENV TRUNCATE_DIM=256 +ENV VOCAB_PATH=public/vocab_en_es.txt +ENV VOCAB_EMBEDDINGS_PATH=public/vocab_embeddings.npz +ENV SAE_DEVICE=CPU + +# Entrypoint: FastAPI serving API + static frontend +CMD ["uv", "run", "--directory", "backend", "--frozen", "python", "-m", "server"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..1151030ebc77eb849dd4fbcbc60f52d5876d809a --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Hector Bauzan + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000000000000000000000000000000000000..9d9dbbc7f7d95f0c095ed714ef26e7d86757136e --- /dev/null +++ b/NOTICE @@ -0,0 +1,4 @@ +VHectorLab 3D +Copyright 2026 Hector Bauzan + +This product includes software developed for the VHectorLab 3D study / laboratory tool. diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..83445630f5656223fecba79c696e6a298a503e4f --- /dev/null +++ b/README.md @@ -0,0 +1,206 @@ +--- +title: VHectorLab 3D +emoji: 🧭 +colorFrom: gray +colorTo: blue +sdk: docker +app_port: 7860 +pinned: false +license: apache-2.0 +short_description: VHectorLab 3D - Semantic embeddings study tool in WebGL +--- + +# VHectorLab 3D + +**A local study / laboratory tool for exploring semantic embeddings in 3D (WebGL). Not a production SaaS.** + + +3D visualization (WebGL/Three.js) and semantic embedding vector arithmetic (`A − B + C`), plus compare sequences and related lab experiments. + +| | | +| :--- | :--- | +| **License** | [Apache-2.0](./LICENSE) © 2026 Hector Bauzan | +| **Public demo** | [Hugging Face Space](https://huggingface.co/spaces/hbauzan/llm-semantic-visualizer) | +| **Local (macOS)** | `./setup.sh` → option `1` → http://127.0.0.1:5173 | + +--- + +## Public demo (no install) + +Try the hosted **cpu-basic** demo (same profile as `local-full`: Arctic-m-v2 @ 256-D, EN∪ES vocab): + +👉 **https://huggingface.co/spaces/hbauzan/llm-semantic-visualizer** + +That Space is a shared study sandbox (Docker, CPU). Expect cold starts, variable latency, and soft resource limits. There is **no SLA**, no multi-tenant isolation, and no auth — please be gentle (avoid automated flooding of `/embed`, `/compare`, `/arithmetic`). + +For serious work or heavier SAE/training experiments, run **locally** on macOS (below). + +--- + +## Platform support + +**Created and tested on macOS 26.5.1 (Darwin 25.5.0, Apple Silicon).** + +This project was **not** prepared or validated for Windows or Linux. It may work there with a few adjustments (package managers, paths, process control), but that is unsupported. Prefer a Mac matching the versions above. + +The Hugging Face Space image runs on Linux CPU inside HF’s Docker runtime — that path is supported for the **demo**, not as a general Linux desktop install guide. + +--- + +## Quick start (recommended) + +Everything goes through `./setup.sh`. On macOS, **option 1 installs missing tools for you** when needed. + +```bash +# 1. Clone the repo (folder name matches the GitHub repo) +git clone https://github.com/hbauzan/vhectorlab.git +cd vhectorlab + +# 2. Open the control panel +chmod +x setup.sh # first time only, if needed +./setup.sh +``` + +Choose **option `1`** (`Deploy / Start Tool`). + +That option will: + +1. Probe backend (`:8000`) and frontend (`:5173`): matching process **and** health check +2. **If both healthy**: skip install/sync, tests, and start; open the browser +3. **If either is sick** (port/process/health mismatch, e.g. frozen Vite): recycle both (stop + start after tests) +4. **If only one is healthy**: restart **both** services after prereqs + tests +5. **If both down**: check/install prerequisites on macOS (`uv`, Homebrew+Node if needed, `.env`, `uv sync`, `npm install` if needed), ensure vocab, run tests, start both, open browser +6. After a fresh start: stream live backend logs (`Ctrl+C` pauses the tail — services stay up) + +App URL when ready: + +👉 **http://127.0.0.1:5173** (VHectorLab 3D — Magic Workbench chrome) + +- Pause live logs: `Ctrl+C` → **Enter** returns to the menu; **Ctrl+C** again exits the panel (services keep running) +- Stop services: option **`10`** only (Ctrl+C never stops the stack) + +> First run can take a while: it may download Homebrew/Node/`uv`, Python packages, and the embedding model. +> Re-running option **1** while the stack is already healthy will **not** bounce the servers or re-run tests. + +--- + +## What `setup.sh` expects (and installs on Mac) + +| Tool | Role | If missing on macOS | +| :--- | :--- | :--- | +| **uv** | Python deps / run backend | Installed via [official uv installer](https://docs.astral.sh/uv/) | +| **Node.js + npm** | Frontend (Vite / Vitest) | Installed via Homebrew (`brew install node`) | +| **Homebrew** | Used only to install Node if needed | Installed from [brew.sh](https://brew.sh) | +| **`.env`** | Runtime config | Copied from `.env.example` | +| **Backend deps** | FastAPI / PyTorch stack | `uv sync --extra dev` in `backend/` | +| **Frontend deps** | Three.js / Vite | `npm install` when `node_modules` is absent | +| **Docker Desktop** | **Optional** — only for option **7** (HF Spaces image build) | Not installed by option 1. Option 7 checks Docker; on macOS it can install the Docker Desktop cask via Homebrew and asks you to start the app | + +**Daily local use (option 1) does not need Docker Desktop** — only `uv` + Node/`npm` (+ network). + +You do **not** need to install `uv` or Node by hand on a typical Mac — option 1 handles that. You *do* need network access and (for Homebrew) permission to install software. + +### Optional: Docker Desktop (option 7) + +Install [Docker Desktop for Mac](https://www.docker.com/products/docker-desktop/) if you want to build the Hugging Face Spaces image locally. After install, open Docker Desktop and wait until the engine is running (whale icon steady), then use option **7**. + +Option **8** publishes a **Docker** Space on **cpu-basic** via the `hf` CLI. It injects `deploy/hf/space-frontmatter.yml` into the Space README at push time (GitHub `README.md` stays clean) and does **not** require Docker Desktop locally. + +--- + +## `setup.sh` menu + +| Option | What it does | +| :--- | :--- | +| **1** | Deploy/start: **idempotent** — if both healthy, only open browser; else prereqs→tests→start (**no Docker**) | +| **2** | Backend only (`:8000`) — **skip if already healthy**; refuse if sick | +| **3** | System heartbeat / health check | +| **4** | Frontend unit tests (Vitest) | +| **5** | Backend unit tests (pytest) | +| **6** | Vocabulary: load a custom file or generate N words | +| **7** | Build HF Spaces Docker image locally (torch CPU · optional :7860 smoke) | +| **8** | Publish HF Space: inject `deploy/hf/space-frontmatter.yml` into README at push (GitHub README stays clean) | +| **9** | View backend logs | +| **10** | **Stop** / clean services (always kills; not idempotent) | +| **0** | Exit | + +--- + +## Environment variables + +Copy `.env.example` → `.env` (option 1 does this if `.env` is missing). Common keys: + +| Variable | Default | Description | +| :--- | :--- | :--- | +| `HOST` / `PORT` | `127.0.0.1` / `8000` | Backend listen address | +| `MODEL_NAME` | `all-mpnet-base-v2` | Embedding model | +| `VOCAB_PATH` | `public/vocab.txt` | App vocabulary file (use `public/vocab_en_es.txt` for EN∪ES) | +| `VITE_API_BASE_URL` | `/api` | Browser API base URL | +| `VITE_SHOW_CAM_POSE` | `false` | Live camera POS/ROT overlay (debug) | +| `VITE_AMIGA_PEN_0`…`_7` | MagicWB pens | Workbench theme palette pens (`#RRGGBB`) | +| `VITE_AMIGA_BG` / `_FG` / `_ACCENT` | `#222222` / `#F0F0F0` / `#3B67A2` | Page/panel bg, text, titlebar accent | + +### EN + ES vocabulary + +Default `public/vocab.txt` is English-only. For multilingual Arithmetic/Compare neighbors: + +```bash +uv run python scripts/merge_vocab_en_es.py +# → public/vocab_en_es.txt (~10k EN ∪ ES seed; includes king/rey … cat/gato) +``` + +Set `VOCAB_PATH=public/vocab_en_es.txt`, then regenerate embeddings (`scripts/precompute_vocab_embeddings.py` or **setup.sh option 11**). Languages beyond EN+ES are out of scope for now. + +### Embedding model / profile (setup option 11) + +From `./setup.sh` → **11. Select Embedding Model / Profile**: pick `P#` / `M#`, stage `.env`, rebuild `vocab_embeddings.npz` (EN∪ES), restart backend, print `/health`. On failure the previous `.env` is restored. HF Space options 7/8 are unchanged. + +Default `.env.example` keeps **`all-mpnet-base-v2`** (EN baseline). Switch to `local-comfort` / `local-full` via option 11 when you want multilingual. + +Cross-lingual smoke (after option 11, non-blocking): + +```bash +uv run --directory backend python ../scripts/smoke_crosslingual_cosine.py +uv run --directory backend python ../scripts/smoke_crosslingual_cosine.py --profile local-comfort --include-fr +# Fail only if mean EN↔ES cosine is soft-low: +uv run --directory backend python ../scripts/smoke_crosslingual_cosine.py --strict --threshold 0.35 +``` + +--- + +## Manual start (without the panel) + +Only if you prefer not to use `setup.sh` (still assumes macOS + tools already available): + +```bash +# Backend +cd backend +uv sync --extra dev +uv run python -m server + +# Frontend (another terminal, repo root) +npm install +npx vite --port 5173 --host 127.0.0.1 +``` + +--- + +## Troubleshooting + +| Symptom | What to check | +| :--- | :--- | +| Unsupported platform warning | You are not on Darwin/macOS — unsupported; adapt paths/package managers yourself | +| Homebrew install asks for a password | Normal on first install; approve locally | +| `uv` / `npm` still missing after option 1 | Open a **new** terminal (PATH refresh), then re-run `./setup.sh` | +| Backend tests slow / fail on first run | Model download needs network; wait and retry | +| Browser does not open | Open http://127.0.0.1:5173 manually | +| Port already in use / option 1 says **sick** | Option 1 now recycles automatically. Or use option `10`, then start again | +| Option 1 restarts everything every time | It should not when both are healthy — report a bug if it still kills/relaunches a healthy stack | +| Option 7 fails / “Docker daemon not running” | Install/open **Docker Desktop**, wait until it is running, retry option 7 | +| `Failed to spawn: pytest` / No such file | Stale `backend/.venv` after renaming the folder. Option 1 now recreates it; or run `rm -rf backend/.venv && cd backend && uv sync --extra dev` | + +--- + +## License + +Apache License 2.0 — see [LICENSE](./LICENSE) and [NOTICE](./NOTICE). diff --git a/architecture_spec.md b/architecture_spec.md new file mode 100644 index 0000000000000000000000000000000000000000..4de84825f86e3d8c9d875b212cdd8f42a285a609 --- /dev/null +++ b/architecture_spec.md @@ -0,0 +1,113 @@ +# Architecture Specification - VHectorLab 3D + +## System Architecture + +VHectorLab 3D is a 3D semantic vector visualizer and vector arithmetic explorer. + +### Backend Tier (`backend/`) +- **Framework**: FastAPI with Uvicorn. +- **Model Interface**: Lazy-loaded `SentenceTransformer` via catalog selection (`MODEL_PROFILE` / `MODEL_NAME` / `TRUNCATE_DIM`) in FastAPI `lifespan`. Catalog module: `backend/model_catalog.py` (`resolve_selection_from_env`, `build_model`, `encode_texts`). +- **Encode adapter**: All query/vocab encoding goes through `encode_texts` (E5 `query:` prefixes when `e5_mode`, optional Matryoshka `truncate_dim`, L2 normalize). Frontend never applies model-specific quirks. +- **Vocabulary Acceleration**: Pre-computed L2-normalized embedding matrix $(N \times D)$ kept in RAM for instant matrix-vector dot product cosine similarity calculation: + $$\text{Sim}(V_{res}, V_{vocab}) = V_{vocab} \cdot V_{res}^T$$ +- **SAE dim safety**: On load, if session SAE `input_dim` ≠ active `embedding_dim`, SAE RAM + checkpoint are cleared automatically. +- **Vocab NPZ**: Prefer `VOCAB_EMBEDDINGS_PATH` when present. Keys: `words`, `embeddings`, `model_name`, `embedding_dim`, optional `truncate_dim`. **Mismatch strategy**: if `model_name` or effective width disagrees with the active catalog selection, log a loud warning, re-encode from `VOCAB_PATH` via `encode_texts`, and overwrite the NPZ (local DX auto-rebuild — not a hard fail). +- **CORS Policy**: `allow_origins=["*"]` with `allow_credentials=False` for cross-origin WebGL clients. + +### API Surface +- `GET /health`: Server status, model Hub id, optional `model_profile`, `embedding_dim`, `truncate_dim`, vocabulary size, and runtime `device` (`cpu`|`cuda`|`mps`). +- `POST /embed`: Computes embedding vector for input text. +- `POST /tokenize`: Returns tokenization details. +- `POST /arithmetic`: Computes $V_{res} = V_A - V_B + V_C$ and returns top-$K$ nearest vocabulary words and component vectors. +- `POST /compare`: Batch-encodes 1–1024 texts, L2-normalizes embeddings, and returns per-item cosine vs the first token (anchor). +- `POST /project`: Projects precomputed embedding vectors to 2D/3D (does **not** re-encode text). v1 method = **`umap` only**; `pca` / `tsne` → **501**; other methods → **400**. Default seed `42`. When `dim > 50`, internal PCA→50 before UMAP. Positions are zero-mean and RMS-scaled server-side. + +### Data Contracts + +#### Health +```json +{ + "status": "ok", + "model": "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", + "model_profile": "local-comfort", + "short_label": "MiniLM-multi", + "embedding_dim": 384, + "truncate_dim": null, + "device": "mps", + "is_loaded": true, + "vocab_size": 12345 +} +``` +`model_profile` / `truncate_dim` / `short_label` may be `null` when unset. `embedding_dim` is the effective width after truncate. Navbar ONLINE chip uses profile (if any) · short_label · `{D}D` · device. + +#### Arithmetic +```json +{ + "word_a": "king", + "word_b": "man", + "word_c": "woman", + "top_k": 10 +} +``` +Returns: +```json +{ + "inputs": {"word_a": "king", "word_b": "man", "word_c": "woman"}, + "vector_res": [0.01, ..., 0.05], + "components": {"vec_a": [...], "vec_b": [...], "vec_c": [...]}, + "results": [ + {"word": "queen", "score": 0.892, "token_id": 42} + ] +} +``` + +#### Compare +```json +{ "texts": ["king", "queen", "man"] } +``` +Returns: +```json +{ + "count": 3, + "anchor": { "index": 0, "text": "king" }, + "items": [ + { + "id": "tok_0", + "index": 0, + "text": "king", + "embedding": [0.01, "..."], + "cosine_vs_first": 1.0 + } + ] +} +``` +`cosine_vs_first` is $\text{dot}(\hat{e}_i, \hat{e}_0)$ on L2-normalized embeddings. Frontend reorders may recompute scores in memory without re-calling `/compare`. + +#### Project (Galaxy / UMAP) +```json +{ + "vectors": [[0.01, "..."], ["..."]], + "method": "umap", + "n_components": 3, + "seed": 42, + "params": { "n_neighbors": 15, "min_dist": 0.1, "metric": "cosine" } +} +``` +Constraints: `len(vectors)` ∈ 1..1024; uniform row dim; `n_components` ∈ {2, 3}; `method` = `umap` (green path). + +Returns: +```json +{ + "method": "umap", + "n_components": 3, + "positions": [[x, y, z], "..."], + "meta": { + "seed": 42, + "n_neighbors": 15, + "min_dist": 0.1, + "metric": "cosine", + "pre_pca_dims": 50 + } +} +``` +`meta.pre_pca_dims` is present only when the internal PCA pre-step ran. Encoding stays on `/compare` (or SAE encode); `/project` is additive. diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000000000000000000000000000000000000..05223a69a3abbba1097d417129b9dd1f11dbce23 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,21 @@ +# VHectorLab 3D — Backend + +FastAPI service for this **study / laboratory** tool: semantic embeddings, vector arithmetic (`A − B + C`), tokenize, and compare APIs that drive the WebGL UI. + +This is **not** a production multi-tenant API. Prefer the [public Hugging Face demo](https://huggingface.co/spaces/hbauzan/llm-semantic-visualizer) for a quick look, or run the full stack locally via the root [`../README.md`](../README.md) (`./setup.sh`). + +## Requirements & setup + +From this directory (macOS + [`uv`](https://docs.astral.sh/uv/)): + +```bash +uv sync --extra dev +uv run pytest +uv run python -m server +``` + +Default listen address comes from the repo `.env` / `.env.example` (`HOST` / `PORT`, typically `127.0.0.1:8000`). + +## License + +Apache License 2.0 — see [`../LICENSE`](../LICENSE) and [`../NOTICE`](../NOTICE). diff --git a/backend/artifacts/.gitignore b/backend/artifacts/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..50558a850fc8c51d8f6dedd5b42cbad15b56cc04 --- /dev/null +++ b/backend/artifacts/.gitignore @@ -0,0 +1,3 @@ +# SAE checkpoints (large binaries) — train locally; do not commit weights. +*.pt +!.gitkeep diff --git a/backend/artifacts/.gitkeep b/backend/artifacts/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/artifacts/README.md b/backend/artifacts/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ab6a0baa64590ed9b375b53903329ba9bd25f7e1 --- /dev/null +++ b/backend/artifacts/README.md @@ -0,0 +1,6 @@ +# SAE artifacts + +Compare-scope Top-K SAE checkpoint: `sae_weights.pt` (gitignored). + +Created by `POST /api/sae/train`. Deleted by `POST /api/sae/clear` (Retrain confirm in UI). +Loaded automatically on `/api/sae/status` and `/api/sae/encode`. diff --git a/backend/crosslingual_smoke.py b/backend/crosslingual_smoke.py new file mode 100644 index 0000000000000000000000000000000000000000..55323d7037deee5236fe7d11cf2aa71961e52aa9 --- /dev/null +++ b/backend/crosslingual_smoke.py @@ -0,0 +1,66 @@ +""" +Cross-lingual cosine helpers for smoke harness (multillm Slice 7). + +Pure math + pair tables — encode happens in the CLI script. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np +from backend.vocab_merge import DEMO_PAIRS + +# Encode-only FR triads (FR need not be in vocab file). +FR_TRIADS: tuple[tuple[str, str, str], ...] = ( + ("brother", "hermano", "frère"), + ("city", "ciudad", "ville"), + ("sun", "sol", "soleil"), + ("book", "libro", "livre"), + ("truth", "verdad", "vérité"), +) + +DEFAULT_SOFT_THRESHOLD = 0.35 + + +@dataclass(frozen=True, slots=True) +class PairScore: + left: str + right: str + cosine: float + + +def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float: + va = np.asarray(a, dtype=np.float64).reshape(-1) + vb = np.asarray(b, dtype=np.float64).reshape(-1) + if va.shape != vb.shape: + raise ValueError(f"dim mismatch: {va.shape} vs {vb.shape}") + na = float(np.linalg.norm(va)) + nb = float(np.linalg.norm(vb)) + if na == 0.0 or nb == 0.0: + return 0.0 + return float(np.dot(va, vb) / (na * nb)) + + +def score_pairs( + embeddings: dict[str, np.ndarray], + pairs: tuple[tuple[str, str], ...] = DEMO_PAIRS, +) -> list[PairScore]: + scores: list[PairScore] = [] + for left, right in pairs: + if left not in embeddings or right not in embeddings: + raise KeyError(f"missing embedding for pair {left!r}/{right!r}") + scores.append( + PairScore( + left=left, + right=right, + cosine=cosine_similarity(embeddings[left], embeddings[right]), + ) + ) + return scores + + +def mean_cosine(scores: list[PairScore]) -> float: + if not scores: + return 0.0 + return float(sum(s.cosine for s in scores) / len(scores)) diff --git a/backend/device.py b/backend/device.py new file mode 100644 index 0000000000000000000000000000000000000000..459336b7600a5783141764d43dfaae65c829606a --- /dev/null +++ b/backend/device.py @@ -0,0 +1,26 @@ +"""Resolve torch runtime device from env (lazy torch import — safe for AppState top-level).""" + +from __future__ import annotations + + +def get_optimal_device(env_device: str = "AUTO") -> str: + """Resolve device string: AUTO|CPU|CUDA|MPS|GPU → cuda|mps|cpu.""" + env_device = (env_device or "AUTO").upper().strip() + if env_device == "CPU": + return "cpu" + + import torch + + if env_device == "CUDA": + return "cuda" if torch.cuda.is_available() else "cpu" + if env_device == "MPS": + if hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): + return "mps" + return "cpu" + if env_device in ("GPU", "AUTO"): + if torch.cuda.is_available(): + return "cuda" + if hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): + return "mps" + return "cpu" + return "cpu" diff --git a/backend/model_catalog.py b/backend/model_catalog.py new file mode 100644 index 0000000000000000000000000000000000000000..1b789fd71a4a887cd62664ba4b2a9c10525e6fbd --- /dev/null +++ b/backend/model_catalog.py @@ -0,0 +1,442 @@ +""" +Embedding model catalog — single source of truth for profiles and Hub IDs. + +Resolves selection from env, builds SentenceTransformer, and encodes texts +with E5 prefixes / Matryoshka truncate / L2 norm in one adapter. +""" + +from __future__ import annotations + +import logging +import os +from collections.abc import Sequence +from dataclasses import dataclass, replace +from typing import Any + +import numpy as np + +logger = logging.getLogger(__name__) + +# Explicit no-go list from roadmap §2.3 (must never appear in list_models). +NO_GO_HUB_IDS: frozenset[str] = frozenset( + { + "BAAI/bge-m3", + "jinaai/jina-embeddings-v3", + } +) + + +@dataclass(frozen=True, slots=True) +class ModelSelection: + """Resolved embedding selection for env / setup / AppState (Slice 2+).""" + + hub_id: str + profile: str | None + trust_remote_code: bool + e5_mode: bool + truncate_dim: int | None + gated: bool + short_label: str + + +@dataclass(frozen=True, slots=True) +class _ModelEntry: + hub_id: str + short_label: str + trust_remote_code: bool = False + e5_mode: bool = False + default_truncate_dim: int | None = None + gated: bool = False + + +@dataclass(frozen=True, slots=True) +class ProfileInfo: + id: str + hub_id: str + default_truncate_dim: int | None + + +# §2.1 required + §2.2 inferred extras (order = menu order later). +_MODELS: tuple[_ModelEntry, ...] = ( + _ModelEntry( + hub_id="sentence-transformers/all-mpnet-base-v2", + short_label="EN baseline (mpnet)", + ), + _ModelEntry( + hub_id="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", + short_label="MiniLM-multi", + ), + _ModelEntry( + hub_id="Snowflake/snowflake-arctic-embed-m-v2.0", + short_label="Arctic-m-v2", + trust_remote_code=True, + default_truncate_dim=256, + ), + _ModelEntry( + hub_id="Alibaba-NLP/gte-multilingual-base", + short_label="GTE-multi", + trust_remote_code=True, + ), + _ModelEntry( + hub_id="intfloat/multilingual-e5-small", + short_label="E5-small-multi", + e5_mode=True, + ), + _ModelEntry( + hub_id="google/embeddinggemma-300m", + short_label="EmbeddingGemma-300m", + gated=True, + ), + _ModelEntry( + hub_id="sentence-transformers/paraphrase-multilingual-mpnet-base-v2", + short_label="mpnet-multi", + ), + _ModelEntry( + hub_id="intfloat/multilingual-e5-base", + short_label="E5-base-multi", + e5_mode=True, + ), + _ModelEntry( + hub_id="sentence-transformers/distiluse-base-multilingual-cased-v2", + short_label="distiluse-multi", + ), +) + +_MODEL_BY_HUB: dict[str, _ModelEntry] = {m.hub_id: m for m in _MODELS} + +# Named profiles (§2.4). Space Docker pins `local-full` (see Dockerfile). +# `hf-demo` remains a local MiniLM preset (comfort twin), not the live Space model. +_PROFILES: dict[str, ProfileInfo] = { + "local-comfort": ProfileInfo( + id="local-comfort", + hub_id="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", + default_truncate_dim=None, + ), + "local-full": ProfileInfo( + id="local-full", + hub_id="Snowflake/snowflake-arctic-embed-m-v2.0", + default_truncate_dim=256, + ), + "hf-demo": ProfileInfo( + id="hf-demo", + hub_id="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", + default_truncate_dim=None, + ), +} + + +def _selection_from_entry( + entry: _ModelEntry, + *, + profile: str | None, + truncate_dim: int | None, +) -> ModelSelection: + return ModelSelection( + hub_id=entry.hub_id, + profile=profile, + trust_remote_code=entry.trust_remote_code, + e5_mode=entry.e5_mode, + truncate_dim=truncate_dim, + gated=entry.gated, + short_label=entry.short_label, + ) + + +def list_models() -> Sequence[ModelSelection]: + """Catalog models as ModelSelection with profile=None and entry default truncate.""" + return tuple( + _selection_from_entry( + entry, + profile=None, + truncate_dim=entry.default_truncate_dim, + ) + for entry in _MODELS + ) + + +def list_profiles() -> Sequence[ProfileInfo]: + """Named profiles in stable order: comfort, full, hf-demo.""" + order = ("local-comfort", "local-full", "hf-demo") + return tuple(_PROFILES[pid] for pid in order) + + +def resolve_profile(profile_id: str) -> ModelSelection: + """Map a profile id to a full ModelSelection (including default truncate_dim).""" + info = _PROFILES.get(profile_id) + if info is None: + raise ValueError(f"unknown profile: {profile_id!r}") + entry = _MODEL_BY_HUB.get(info.hub_id) + if entry is None: + raise ValueError( + f"profile {profile_id!r} maps to missing catalog hub_id {info.hub_id!r}" + ) + return _selection_from_entry( + entry, + profile=info.id, + truncate_dim=info.default_truncate_dim, + ) + + +def _lookup_entry(hub_id: str) -> _ModelEntry | None: + """Exact Hub ID or bare name matching a catalog suffix.""" + entry = _MODEL_BY_HUB.get(hub_id) + if entry is not None: + return entry + bare = hub_id.split("/")[-1] + matches = [m for m in _MODELS if m.hub_id == bare or m.hub_id.endswith("/" + bare)] + if len(matches) == 1: + return matches[0] + return None + + +def get_model(hub_id: str) -> ModelSelection: + """Look up a catalog model by Hub ID or bare name (no profile).""" + entry = _lookup_entry(hub_id) + if entry is None: + raise ValueError(f"unknown model: {hub_id!r}") + return _selection_from_entry( + entry, + profile=None, + truncate_dim=entry.default_truncate_dim, + ) + + +def _parse_truncate_dim(raw: str | int | None) -> int | None: + if raw is None: + return None + if isinstance(raw, int): + return raw if raw > 0 else None + text = str(raw).strip() + if not text: + return None + value = int(text) + if value <= 0: + raise ValueError(f"TRUNCATE_DIM must be positive, got {value}") + return value + + +def _passthrough_selection(hub_id: str, truncate_dim: int | None) -> ModelSelection: + """Legacy / unlisted Hub ID — minimal flags inferred from the id string.""" + lower = hub_id.lower() + return ModelSelection( + hub_id=hub_id, + profile=None, + trust_remote_code=False, + e5_mode="e5" in lower, + truncate_dim=truncate_dim, + gated=False, + short_label=hub_id.split("/")[-1], + ) + + +def resolve_selection( + *, + profile: str | None = None, + model_name: str | None = None, + truncate_dim: int | None = None, +) -> ModelSelection: + """ + Resolve ModelSelection from explicit knobs. + + Profile wins over model_name for hub_id. truncate_dim overrides profile/model defaults + when provided (including overriding local-full's 256). + """ + if profile and profile.strip(): + sel = resolve_profile(profile.strip()) + if truncate_dim is not None: + return replace(sel, truncate_dim=truncate_dim) + return sel + + name = (model_name or "").strip() or "sentence-transformers/all-mpnet-base-v2" + entry = _lookup_entry(name) + if entry is None: + return _passthrough_selection(name, truncate_dim) + sel = _selection_from_entry( + entry, + profile=None, + truncate_dim=entry.default_truncate_dim, + ) + if truncate_dim is not None: + return replace(sel, truncate_dim=truncate_dim) + return sel + + +def resolve_selection_from_env( + *, + profile: str | None = None, + model_name: str | None = None, + truncate_dim: str | int | None = None, +) -> ModelSelection: + """Read MODEL_PROFILE / MODEL_NAME / TRUNCATE_DIM (args override env).""" + env_profile = profile if profile is not None else os.getenv("MODEL_PROFILE") + env_model = model_name if model_name is not None else os.getenv("MODEL_NAME") + if truncate_dim is not None: + dim = _parse_truncate_dim(truncate_dim) + else: + dim = _parse_truncate_dim(os.getenv("TRUNCATE_DIM")) + return resolve_selection( + profile=env_profile, + model_name=env_model, + truncate_dim=dim, + ) + + +def _gte_auto_model(st_model: Any) -> Any | None: + """Return the underlying HF auto model for a SentenceTransformer, if present.""" + try: + first = st_model[0] + except (TypeError, IndexError, KeyError): + return None + return getattr(first, "auto_model", None) + + +def _repair_gte_nonpersistent_buffers(st_model: Any) -> None: + """ + Re-init GTE/Arctic non-persistent buffers after transformers 5.x meta load. + + Remote GTE code registers position_ids / RoPE caches with persistent=False but + does not restore them in _init_weights. transformers≥5 materializes on meta and + leaves those buffers as uninitialized / zero storage → RoPE NaNs or IndexError. + See HF transformers#43644 / #43950. + """ + import torch + + auto = _gte_auto_model(st_model) + if auto is None: + return + embeddings = getattr(auto, "embeddings", None) + if embeddings is None: + return + + position_ids = getattr(embeddings, "position_ids", None) + if isinstance(position_ids, torch.Tensor) and position_ids.numel() > 0: + expected = torch.arange( + position_ids.numel(), + device=position_ids.device, + dtype=position_ids.dtype, + ) + if not torch.equal(position_ids, expected): + embeddings.register_buffer( + "position_ids", expected, persistent=False + ) + logger.info("Repaired GTE embeddings.position_ids after meta load") + + rotary = getattr(embeddings, "rotary_emb", None) + if rotary is None: + return + dim = int(getattr(rotary, "dim", 0) or 0) + base = float(getattr(rotary, "base", 10000.0) or 10000.0) + max_pos = int( + getattr(rotary, "max_position_embeddings", 0) + or getattr(auto.config, "max_position_embeddings", 0) + or 0 + ) + if dim <= 0 or max_pos <= 0: + return + device = next(auto.parameters()).device + inv_freq = 1.0 / ( + base ** (torch.arange(0, dim, 2, device=device, dtype=torch.float32) / dim) + ) + rotary.register_buffer("inv_freq", inv_freq, persistent=False) + # Prefer the module's own cache builder (covers NTKScalingRotaryEmbedding). + if hasattr(rotary, "_set_cos_sin_cache"): + rotary._set_cos_sin_cache( + seq_len=max_pos, + device=device, + dtype=torch.get_default_dtype(), + ) + logger.info("Repaired GTE rotary_emb buffers after meta load") + + +def build_model(selection: ModelSelection, device: str) -> Any: + """Construct SentenceTransformer for the selection (trust_remote_code when declared). + + Arctic (GTE remote code) ships with use_memory_efficient_attention=true and + unpad_inputs=true, which hard-require xformers. That package is often + unavailable on macOS/MPS and unnecessary for lab encode workloads, so we + force both flags off via config_kwargs. Also repairs non-persistent RoPE / + position buffers corrupted by transformers≥5 meta-device loading. + """ + from sentence_transformers import SentenceTransformer + + kwargs: dict[str, Any] = {"device": device} + if selection.trust_remote_code: + kwargs["trust_remote_code"] = True + # Arctic Hub config: use_memory_efficient_attention=true (+ unpad_inputs) + # hard-requires xformers; without MEA, unpad+RoPE blows up on encode. + # Force both off for CPU/MPS/lab loads (HF discussion #15). + kwargs["config_kwargs"] = { + "use_memory_efficient_attention": False, + "unpad_inputs": False, + } + logger.info( + "Building SentenceTransformer hub_id=%s trust_remote_code=%s device=%s", + selection.hub_id, + selection.trust_remote_code, + device, + ) + model = SentenceTransformer(selection.hub_id, **kwargs) + if selection.trust_remote_code: + _repair_gte_nonpersistent_buffers(model) + return model + + +def _apply_e5_prefixes(texts: Sequence[str]) -> list[str]: + """Symmetric lab tasks use query: for all strings (E5 STS guidance).""" + out: list[str] = [] + for text in texts: + t = text.strip() + if t.startswith(("query:", "passage:")): + out.append(t) + else: + out.append(f"query: {t}") + return out + + +def _l2_normalize(matrix: np.ndarray) -> np.ndarray: + if matrix.ndim == 1: + norm = float(np.linalg.norm(matrix)) + if norm == 0: + norm = 1e-9 + return (matrix / norm).astype(np.float32, copy=False) + norms = np.linalg.norm(matrix, axis=1, keepdims=True) + norms[norms == 0] = 1e-9 + return (matrix / norms).astype(np.float32, copy=False) + + +def encode_texts( + model: Any, + texts: str | Sequence[str], + selection: ModelSelection, + *, + show_progress_bar: bool = False, +) -> np.ndarray: + """ + Encode via the catalog adapter: optional E5 prefixes, truncate_dim, L2 normalize. + + Single string → 1-D float32 vector; sequence → 2-D (N, D). + """ + single = isinstance(texts, str) + batch: list[str] = [texts] if single else [str(t) for t in texts] + if selection.e5_mode: + batch = _apply_e5_prefixes(batch) + + raw = model.encode( + batch, show_progress_bar=show_progress_bar, convert_to_numpy=True + ) + arr = np.asarray(raw, dtype=np.float32) + if arr.ndim == 1: + arr = arr.reshape(1, -1) + + if selection.truncate_dim is not None: + dim = selection.truncate_dim + if dim > arr.shape[1]: + raise ValueError( + f"truncate_dim={dim} exceeds model embedding width {arr.shape[1]}" + ) + arr = arr[:, :dim] + + arr = _l2_normalize(arr) + if single: + return arr[0] + return arr diff --git a/backend/model_swap.py b/backend/model_swap.py new file mode 100644 index 0000000000000000000000000000000000000000..e07f8949da7ec90efd35683d193dc605ed0b2e87 --- /dev/null +++ b/backend/model_swap.py @@ -0,0 +1,552 @@ +""" +Embedding model swap helpers for setup.sh option 11. + +Stage .env → precompute vocab NPZ → commit on success; restore backup on failure. +""" + +from __future__ import annotations + +import os +import re +import shutil +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path + +from backend.model_catalog import ( + ModelSelection, + get_model, + list_models, + list_profiles, + resolve_profile, + resolve_selection, +) +from backend.progress_cli import print_fail, print_ok, print_phase +from backend.vocab_merge import merge_en_es_files + +_ENV_KEY_RE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)=(.*)$") + +# ANSI (safe for most terminals used with setup.sh) +_BOLD = "\033[1m" +_DIM = "\033[2m" +_CYAN = "\033[36m" +_MAGENTA = "\033[35m" +_GREEN = "\033[32m" +_YELLOW = "\033[33m" +_RESET = "\033[0m" + + +@dataclass(frozen=True, slots=True) +class EnvSnapshot: + path: Path + backup_path: Path | None + + +@dataclass(frozen=True, slots=True) +class MenuEntry: + """One selectable row in option 11.""" + + index: int + token: str # P1 / M3 / etc. + kind: str # profile | model + selection: ModelSelection + title: str + subtitle: str + + +def repo_root() -> Path: + return Path(__file__).resolve().parent.parent + + +def read_dotenv(path: Path) -> dict[str, str]: + values: dict[str, str] = {} + if not path.is_file(): + return values + for line in path.read_text(encoding="utf-8").splitlines(): + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + match = _ENV_KEY_RE.match(stripped) + if not match: + continue + key, raw = match.group(1), match.group(2) + if len(raw) >= 2 and raw[0] == raw[-1] and raw[0] in ("'", '"'): + raw = raw[1:-1] + values[key] = raw + return values + + +def apply_dotenv(path: Path, *, override: bool = False) -> dict[str, str]: + """ + Load KEY=VAL from a .env file into os.environ. + + By default does not override variables already set in the process environment + (explicit exports / systemd / CI win). Used by the backend so option 1 and + option 11 honor the repo-root `.env` even when `uv run` cwd is `backend/`. + """ + values = read_dotenv(path) + for key, value in values.items(): + if override or key not in os.environ: + os.environ[key] = value + return values + + +def upsert_dotenv(path: Path, updates: dict[str, str | None]) -> None: + """ + Upsert keys in a .env file. Value None removes the key line. + Preserves unrelated lines/comments; appends missing keys at end. + """ + path.parent.mkdir(parents=True, exist_ok=True) + lines: list[str] = [] + if path.is_file(): + lines = path.read_text(encoding="utf-8").splitlines() + + seen: set[str] = set() + out: list[str] = [] + for line in lines: + stripped = line.strip() + match = ( + _ENV_KEY_RE.match(stripped) + if stripped and not stripped.startswith("#") + else None + ) + if match is None: + out.append(line) + continue + key = match.group(1) + if key not in updates: + out.append(line) + continue + seen.add(key) + value = updates[key] + if value is None: + continue + out.append(f"{key}={value}") + + for key, value in updates.items(): + if key in seen or value is None: + continue + out.append(f"{key}={value}") + + text = "\n".join(out) + if text and not text.endswith("\n"): + text += "\n" + path.write_text(text, encoding="utf-8") + + +def stage_env(path: Path, updates: dict[str, str | None]) -> EnvSnapshot: + backup = path.with_suffix(path.suffix + ".multillm.bak") + if path.is_file(): + shutil.copy2(path, backup) + else: + backup.write_text("", encoding="utf-8") + path.write_text("", encoding="utf-8") + upsert_dotenv(path, updates) + return EnvSnapshot(path=path, backup_path=backup) + + +def commit_env(snapshot: EnvSnapshot) -> None: + if snapshot.backup_path and snapshot.backup_path.is_file(): + snapshot.backup_path.unlink() + + +def rollback_env(snapshot: EnvSnapshot) -> None: + if snapshot.backup_path is None: + return + if snapshot.backup_path.is_file(): + shutil.copy2(snapshot.backup_path, snapshot.path) + snapshot.backup_path.unlink() + + +def resolve_choice( + *, profile: str | None = None, model: str | None = None +) -> ModelSelection: + if profile: + return resolve_profile(profile) + if model: + try: + return get_model(model) + except ValueError: + return resolve_selection(model_name=model) + raise ValueError("Provide --profile or --model") + + +def ensure_en_es_vocab(root: Path | None = None) -> Path: + root = root or repo_root() + out = root / "public" / "vocab_en_es.txt" + en = root / "public" / "vocab.txt" + es = root / "public" / "vocab_es.txt" + if not out.is_file(): + merge_en_es_files(en, es, out) + return out + + +def selection_env_updates( + selection: ModelSelection, *, vocab_path: str +) -> dict[str, str | None]: + truncate = str(selection.truncate_dim) if selection.truncate_dim is not None else "" + return { + "MODEL_PROFILE": selection.profile or "", + "MODEL_NAME": selection.hub_id, + "TRUNCATE_DIM": truncate, + "VOCAB_PATH": vocab_path, + } + + +def run_precompute( + selection: ModelSelection, + *, + root: Path | None = None, + vocab_path: Path, + out_path: Path, + device: str = "AUTO", +) -> None: + """Run precompute with live stdout/stderr (tqdm + phase logs visible).""" + root = root or repo_root() + cmd = [ + "uv", + "run", + "python", + str(root / "scripts" / "precompute_vocab_embeddings.py"), + "--model", + selection.hub_id, + "--vocab", + str(vocab_path), + "--out", + str(out_path), + "--device", + device, + ] + if selection.profile: + cmd.extend(["--profile", selection.profile]) + if selection.truncate_dim is not None: + cmd.extend(["--truncate-dim", str(selection.truncate_dim)]) + env = os.environ.copy() + env.setdefault("PYTHONUNBUFFERED", "1") + completed = subprocess.run( + cmd, + cwd=str(root), + env=env, + check=False, + ) + if completed.returncode != 0: + raise RuntimeError(f"precompute failed (exit {completed.returncode})") + + +def apply_model_swap( + selection: ModelSelection, + *, + root: Path | None = None, + env_path: Path | None = None, + device: str = "AUTO", + skip_precompute: bool = False, + quiet: bool = False, +) -> ModelSelection: + """ + Stage .env, ensure EN∪ES vocab, precompute NPZ, commit env. + Rolls back .env on any failure after staging. + """ + root = root or repo_root() + env_path = env_path or (root / ".env") + total = 4 if not skip_precompute else 3 + step = 0 + + def _phase(title: str, detail: str = "") -> None: + nonlocal step + step += 1 + if not quiet: + print_phase(step, total, title, detail=detail) + + _phase( + "Ensure EN∪ES vocabulary", + detail="public/vocab_en_es.txt (merge if missing)", + ) + vocab_en_es = ensure_en_es_vocab(root) + if not quiet: + n_words = sum(1 for line in vocab_en_es.open(encoding="utf-8") if line.strip()) + print_ok(f"Vocab ready — {n_words} words at {vocab_en_es.name}") + + vocab_rel = "public/vocab_en_es.txt" + npz_rel = read_dotenv(env_path).get( + "VOCAB_EMBEDDINGS_PATH", "public/vocab_embeddings.npz" + ) + updates = selection_env_updates(selection, vocab_path=vocab_rel) + + _phase( + "Stage .env (backup → write)", + detail=f"MODEL_NAME={selection.hub_id}", + ) + snapshot = stage_env(env_path, updates) + if not quiet: + print_ok(f"Staged {env_path.name} (rollback available on failure)") + + try: + if not skip_precompute: + _phase( + "Precompute vocab embeddings NPZ", + detail="download/load model + encode (progress bar below)", + ) + run_precompute( + selection, + root=root, + vocab_path=vocab_en_es, + out_path=root / npz_rel, + device=device, + ) + if not quiet: + print_ok(f"NPZ written → {npz_rel}") + + _phase("Commit .env (drop backup)") + commit_env(snapshot) + if not quiet: + print_ok("Environment committed") + print() + trunc = ( + selection.truncate_dim + if selection.truncate_dim is not None + else "—" + ) + print( + f" {_GREEN}{_BOLD}Swap prepare OK{_RESET} " + f"model={selection.short_label} " + f"profile={selection.profile or '—'} " + f"truncate={trunc}" + ) + except Exception: + if not quiet: + print_fail("Failure — restoring previous .env") + rollback_env(snapshot) + raise + return selection + + +def _entry_subtitle(sel: ModelSelection, *, kind: str) -> str: + bits: list[str] = [] + if kind == "profile" and sel.profile: + bits.append(sel.profile) + hub_short = sel.hub_id.split("/")[-1] + bits.append(hub_short) + if sel.truncate_dim is not None: + bits.append(f"MRL→{sel.truncate_dim}D") + if sel.e5_mode: + bits.append("E5") + if sel.gated: + bits.append("gated") + if sel.trust_remote_code: + bits.append("trust_remote") + return " · ".join(bits) + + +def build_menu_entries() -> list[MenuEntry]: + entries: list[MenuEntry] = [] + idx = 1 + for i, p in enumerate(list_profiles(), start=1): + sel = resolve_profile(p.id) + entries.append( + MenuEntry( + index=idx, + token=f"P{i}", + kind="profile", + selection=sel, + title=sel.short_label, + subtitle=_entry_subtitle(sel, kind="profile"), + ) + ) + idx += 1 + for i, m in enumerate(list_models(), start=1): + entries.append( + MenuEntry( + index=idx, + token=f"M{i}", + kind="model", + selection=m, + title=m.short_label, + subtitle=_entry_subtitle(m, kind="model"), + ) + ) + idx += 1 + return entries + + +def format_menu_lines(*, color: bool = True) -> list[str]: + """Pretty catalog listing for option 11 (profiles then models).""" + b = _BOLD if color else "" + d = _DIM if color else "" + c = _CYAN if color else "" + m = _MAGENTA if color else "" + g = _GREEN if color else "" + y = _YELLOW if color else "" + r = _RESET if color else "" + + entries = build_menu_entries() + lines: list[str] = [] + lines.append(f"{c}{b}╭────────────────────────────────────────────────────────────╮{r}") + lines.append(f"{c}{b}│{r} {b}Embedding profiles{r} {d}(recommended presets){r}") + lines.append(f"{c}{b}╰────────────────────────────────────────────────────────────╯{r}") + + for e in entries: + if e.kind != "profile": + continue + mark = f"{g}★{r}" if color else "*" + lines.append( + f" {b}{e.index:>2}){r} {mark} {m}{e.token:<4}{r} " + f"{b}{e.title:<22}{r} {d}{e.subtitle}{r}" + ) + + lines.append("") + lines.append(f"{c}{b}╭────────────────────────────────────────────────────────────╮{r}") + lines.append(f"{c}{b}│{r} {b}Catalog models{r} {d}(pick any one){r}") + lines.append(f"{c}{b}╰────────────────────────────────────────────────────────────╯{r}") + + for e in entries: + if e.kind != "model": + continue + lines.append( + f" {b}{e.index:>2}){r} {y}{e.token:<4}{r} " + f"{b}{e.title:<22}{r} {d}{e.subtitle}{r}" + ) + + lines.append("") + lines.append(f" {b} 0){r} {d}Cancel{r}") + lines.append( + f" {d}Enter a number (1–{entries[-1].index}), or P# / M# token.{r}" + ) + return lines + + +def selection_from_menu_token(token: str) -> ModelSelection: + raw = token.strip() + if not raw: + raise ValueError("empty choice") + + if raw.isdigit(): + n = int(raw) + for e in build_menu_entries(): + if e.index == n: + return e.selection + raise ValueError(f"unknown menu index: {n}") + + t = raw.upper() + profiles = list(list_profiles()) + models = list(list_models()) + if t.startswith("P") and t[1:].isdigit(): + idx = int(t[1:]) - 1 + if 0 <= idx < len(profiles): + return resolve_profile(profiles[idx].id) + if t.startswith("M") and t[1:].isdigit(): + idx = int(t[1:]) - 1 + if 0 <= idx < len(models): + return models[idx] + lower = raw + try: + return resolve_profile(lower) + except ValueError: + pass + return resolve_choice(model=lower) + + +def describe_selection(selection: ModelSelection) -> str: + trunc = ( + str(selection.truncate_dim) if selection.truncate_dim is not None else "—" + ) + return ( + f"{selection.short_label} · profile={selection.profile or '—'} " + f"· truncate={trunc} · {selection.hub_id}" + ) + + +def main(argv: list[str] | None = None) -> int: + import argparse + + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="cmd", required=True) + + list_p = sub.add_parser("list", help="Print profiles + catalog short labels") + list_p.add_argument( + "--plain", + action="store_true", + help="Disable ANSI colors", + ) + + apply_p = sub.add_parser( + "apply", help="Stage env, precompute, commit (or rollback)" + ) + apply_p.add_argument("--profile", default=None) + apply_p.add_argument("--model", default=None) + apply_p.add_argument("--choice", default=None, help="Menu token e.g. P1 / M3 / 2") + apply_p.add_argument("--device", default=os.getenv("SAE_DEVICE", "AUTO")) + apply_p.add_argument( + "--skip-precompute", + action="store_true", + help="Only upsert .env (tests / dry wiring)", + ) + apply_p.add_argument("--env", type=Path, default=None) + apply_p.add_argument( + "--quiet", + action="store_true", + help="Suppress phase progress (tests)", + ) + + describe_p = sub.add_parser( + "describe", help="Resolve a choice and print a one-line summary" + ) + describe_p.add_argument("--choice", required=True) + + args = parser.parse_args(argv) + root = repo_root() + + if args.cmd == "list": + print("\n".join(format_menu_lines(color=not args.plain))) + return 0 + + if args.cmd == "describe": + try: + selection = selection_from_menu_token(args.choice) + except Exception as exc: # noqa: BLE001 + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + print(describe_selection(selection)) + return 0 + + if args.cmd == "apply": + if args.choice: + selection = selection_from_menu_token(args.choice) + else: + selection = resolve_choice(profile=args.profile, model=args.model) + env_path = args.env or (root / ".env") + if not args.quiet: + print() + print(f" {_BOLD}Target{_RESET}: {describe_selection(selection)}") + print( + f" {_DIM}Pipeline: vocab → stage .env → encode NPZ → commit{_RESET}" + ) + print() + try: + apply_model_swap( + selection, + root=root, + env_path=env_path, + device=args.device, + skip_precompute=args.skip_precompute, + quiet=args.quiet, + ) + except Exception as exc: # noqa: BLE001 + print(f"ERROR: {exc}", file=sys.stderr) + if "gated" in selection.hub_id.lower() or selection.gated: + print( + "Gated model tip: run `hf auth login` and accept the model license on Hugging Face.", + file=sys.stderr, + ) + return 1 + print( + f"OK model={selection.hub_id} profile={selection.profile!r} " + f"truncate_dim={selection.truncate_dim!r}" + ) + return 0 + + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/backend/perform_tests.py b/backend/perform_tests.py new file mode 100755 index 0000000000000000000000000000000000000000..1c87030afe5ad6f50c1c19c5774bf1086e83db57 --- /dev/null +++ b/backend/perform_tests.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +""" +System Heartbeat and Diagnostic Runner for VHectorLab 3D. +Tests /health and /arithmetic endpoints locally using httpx or direct state. +""" + +import logging +import sys + +import httpx + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("heartbeat") + + +def run_heartbeat(base_url: str = "http://127.0.0.1:8000") -> bool: + logger.info(f"Running System Heartbeat against {base_url}...") + + try: + # 1. Health check + health_resp = httpx.get(f"{base_url}/health", timeout=5.0) + if health_resp.status_code != 200: + logger.error( + f"Health check failed with status {health_resp.status_code}: {health_resp.text}" + ) + return False + + health_data = health_resp.json() + logger.info(f"Health check OK: {health_data}") + + # 2. Arithmetic check (king - man + woman) + payload = {"word_a": "king", "word_b": "man", "word_c": "woman", "top_k": 5} + arith_resp = httpx.post(f"{base_url}/arithmetic", json=payload, timeout=15.0) + if arith_resp.status_code != 200: + logger.error( + f"Arithmetic test failed with status {arith_resp.status_code}: {arith_resp.text}" + ) + return False + + arith_data = arith_resp.json() + results = arith_data.get("results", []) + logger.info("Arithmetic test OK! Top results for 'king - man + woman':") + for res in results: + logger.info(f" - {res['word']} (score: {res['score']:.4f})") + + return True + except Exception as e: # noqa: BLE001 + logger.error(f"Heartbeat connection error: {e}") + return False + + +if __name__ == "__main__": + url = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:8000" + success = run_heartbeat(url) + sys.exit(0 if success else 1) diff --git a/backend/progress_cli.py b/backend/progress_cli.py new file mode 100644 index 0000000000000000000000000000000000000000..5a6be7d02d6993a5423b7c7bd28dc66fdf7f6481 --- /dev/null +++ b/backend/progress_cli.py @@ -0,0 +1,36 @@ +"""Terminal progress helpers for operator-facing CLIs (option 11, precompute).""" + +from __future__ import annotations + +import sys + + +def progress_bar(current: int, total: int, *, width: int = 28) -> str: + total = max(int(total), 1) + current = max(0, min(int(current), total)) + filled = round(width * current / total) + filled = max(0, min(filled, width)) + return "█" * filled + "░" * (width - filled) + + +def print_phase( + current: int, + total: int, + title: str, + *, + detail: str = "", + file=sys.stdout, +) -> None: + bar = progress_bar(current, total) + line = f" [{bar}] {current}/{total} {title}" + print(line, file=file, flush=True) + if detail: + print(f" {detail}", file=file, flush=True) + + +def print_ok(message: str, *, file=sys.stdout) -> None: + print(f" ✓ {message}", file=file, flush=True) + + +def print_fail(message: str, *, file=sys.stderr) -> None: + print(f" ✗ {message}", file=file, flush=True) diff --git a/backend/projection.py b/backend/projection.py new file mode 100644 index 0000000000000000000000000000000000000000..2b8717b571ffbb70e3a0c1ba971bc27b38adc199 --- /dev/null +++ b/backend/projection.py @@ -0,0 +1,203 @@ +""" +Dimensionality reduction for Galaxy VIEW. + +Deep seam: project_embeddings(vectors, ...) → positions + meta. +UMAP only in v1; PCA/t-SNE raise ProjectError(501). +""" + +from __future__ import annotations + +from typing import Any + +import numpy as np + +PRE_PCA_DIMS = 50 +MAX_VECTORS = 1024 +DEFAULT_N_NEIGHBORS = 15 +DEFAULT_MIN_DIST = 0.1 +DEFAULT_METRIC = "cosine" + + +class ProjectError(Exception): + """Validation / capability error with HTTP-ish status code.""" + + def __init__(self, message: str, status_code: int = 400) -> None: + super().__init__(message) + self.message = message + self.status_code = status_code + + +def _as_matrix(vectors: list[list[float]] | np.ndarray) -> np.ndarray: + if isinstance(vectors, np.ndarray): + mat = np.asarray(vectors, dtype=np.float64) + else: + if not vectors: + raise ProjectError("vectors list cannot be empty (need 1..1024)") + lengths = {len(row) for row in vectors} + if len(lengths) != 1: + raise ProjectError("all vectors must have the same dimension") + if next(iter(lengths)) < 1: + raise ProjectError("vector dimension must be >= 1") + mat = np.asarray(vectors, dtype=np.float64) + + if mat.ndim != 2: + raise ProjectError("vectors must be a 2D matrix [n, dim]") + n, dim = mat.shape + if n < 1 or n > MAX_VECTORS: + raise ProjectError(f"len(vectors) must be in 1..{MAX_VECTORS}, got {n}") + if dim < 1: + raise ProjectError("vector dimension must be >= 1") + if not np.isfinite(mat).all(): + raise ProjectError("vectors must contain only finite values") + return mat + + +def _normalize_positions(pos: np.ndarray) -> np.ndarray: + """Zero-mean; scale so RMS distance from origin is 1 (stable camera defaults).""" + centered = pos - pos.mean(axis=0, keepdims=True) + rms = float(np.sqrt(np.mean(np.sum(centered**2, axis=1)))) + if rms < 1e-12: + return centered + return centered / rms + + +def _project_small_n( + mat: np.ndarray, *, n_components: int, seed: int +) -> tuple[np.ndarray, dict[str, Any]]: + """ + Deterministic layout when UMAP cannot run (n < 3). + + n=1 → origin; n=2 → PCA to 1D padded to n_components (pair on an axis). + """ + n_samples, dim = mat.shape + if n_samples == 1: + pos = np.zeros((1, n_components), dtype=np.float64) + return pos, {"fallback": "origin"} + + from sklearn.decomposition import PCA + + k = min(n_samples - 1, n_components, dim) + if k < 1: + pos = np.zeros((n_samples, n_components), dtype=np.float64) + return pos, {"fallback": "origin"} + + reduced = PCA(n_components=k, random_state=seed).fit_transform(mat) + pos = np.zeros((n_samples, n_components), dtype=np.float64) + pos[:, :k] = reduced + return pos, {"fallback": "pca_micro", "pca_components": int(k)} + + +def _resolve_umap_params( + n_samples: int, params: dict[str, Any] | None +) -> dict[str, Any]: + raw = params or {} + n_neighbors = int(raw.get("n_neighbors", DEFAULT_N_NEIGHBORS)) + min_dist = float(raw.get("min_dist", DEFAULT_MIN_DIST)) + metric = str(raw.get("metric", DEFAULT_METRIC)) + + # UMAP requires n_neighbors < n_samples + max_nn = max(2, n_samples - 1) + n_neighbors = max(n_neighbors, 2) + n_neighbors = min(n_neighbors, max_nn) + + if min_dist < 0.0: + raise ProjectError("params.min_dist must be >= 0") + + return { + "n_neighbors": n_neighbors, + "min_dist": min_dist, + "metric": metric, + } + + +def project_embeddings( + vectors: list[list[float]] | np.ndarray, + *, + method: str = "umap", + n_components: int = 3, + seed: int = 42, + params: dict[str, Any] | None = None, +) -> dict[str, Any]: + """ + Project embedding rows to 2D/3D. + + Returns: + { method, n_components, positions, meta } + """ + method_l = (method or "").strip().lower() + if method_l in ("pca", "tsne"): + raise ProjectError( + f"method '{method_l}' is not implemented yet; use 'umap'", + status_code=501, + ) + if method_l != "umap": + raise ProjectError( + f"unsupported method '{method}'; v1 accepts 'umap' only " + "(pca/tsne coming later)", + status_code=400, + ) + + if n_components not in (2, 3): + raise ProjectError("n_components must be 2 or 3") + + mat = _as_matrix(vectors) + n_samples, dim = mat.shape + + # UMAP needs ≥3 samples; 1–2 tokens still valid in Token Comparison / Galaxy. + if n_samples < 3: + embedded, fallback_meta = _project_small_n( + mat, n_components=n_components, seed=seed + ) + positions = _normalize_positions(np.asarray(embedded, dtype=np.float64)) + return { + "method": "umap", + "n_components": n_components, + "positions": positions.tolist(), + "meta": {"seed": seed, **fallback_meta}, + } + + umap_params = _resolve_umap_params(n_samples, params) + pre_pca_dims: int | None = None + work = mat + + if dim > PRE_PCA_DIMS: + from sklearn.decomposition import PCA + + pca_dims = min(PRE_PCA_DIMS, n_samples - 1, dim) + if pca_dims < n_components: + raise ProjectError( + f"cannot pre-reduce: need at least {n_components} PCA dims " + f"(n={n_samples}, dim={dim})" + ) + pca = PCA(n_components=pca_dims, random_state=seed) + work = pca.fit_transform(mat) + pre_pca_dims = pca_dims + + import umap + + reducer = umap.UMAP( + n_components=n_components, + n_neighbors=umap_params["n_neighbors"], + min_dist=umap_params["min_dist"], + metric=umap_params["metric"], + random_state=seed, + n_jobs=1, + ) + embedded = reducer.fit_transform(work) + positions = _normalize_positions(np.asarray(embedded, dtype=np.float64)) + + meta: dict[str, Any] = { + "seed": seed, + "n_neighbors": umap_params["n_neighbors"], + "min_dist": umap_params["min_dist"], + "metric": umap_params["metric"], + } + if pre_pca_dims is not None: + meta["pre_pca_dims"] = pre_pca_dims + + return { + "method": "umap", + "n_components": n_components, + "positions": positions.tolist(), + "meta": meta, + } diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..94273e8ccbf49209dcbfa23e4f0a0cf3056044e9 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,48 @@ +[project] +name = "vhectorlab-backend" +version = "2.3.0" +description = "VHectorLab 3D FastAPI Backend for Vector Arithmetic and Semantic Embeddings" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "fastapi>=0.110.0", + "uvicorn>=0.28.0", + "sentence-transformers>=2.5.0", + "torch>=2.2.0", + "numpy>=1.26.0", + "httpx>=0.27.0", + "huggingface-hub>=0.21.0", + "orjson>=3.11.9", + "umap-learn==0.5.12", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "httpx>=0.27.0", + "ruff>=0.3.0", +] + +# HF Space / Docker (Linux): CPU wheels only — PyPI Linux torch pulls multi-GB nvidia-*. +# macOS keeps default PyPI (MPS-capable). UV_TORCH_BACKEND alone does NOT affect `uv sync`. +[[tool.uv.index]] +name = "pytorch-cpu" +url = "https://download.pytorch.org/whl/cpu" +explicit = true + +[tool.uv.sources] +torch = [ + { index = "pytorch-cpu", marker = "sys_platform == 'linux'" }, +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +pythonpath = ["..", "."] diff --git a/backend/routers/core.py b/backend/routers/core.py new file mode 100644 index 0000000000000000000000000000000000000000..ef3b7d0bee3d73a57a67a862ca211c003fd0a12a --- /dev/null +++ b/backend/routers/core.py @@ -0,0 +1,170 @@ +""" +Core API Router for VHectorLab 3D. +Provides /health, /embed, /tokenize, /arithmetic, /compare, and /project endpoints. +""" + +from typing import Any, Literal + +from backend.projection import ProjectError, project_embeddings +from backend.state import state +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel, Field + +router = APIRouter() + + +class EmbedRequest(BaseModel): + text: str = Field(..., description="Text to compute embedding for") + + +class TokenizeRequest(BaseModel): + text: str = Field(..., description="Text to tokenize") + + +class ArithmeticRequest(BaseModel): + word_a: str = Field( + ..., json_schema_extra={"example": "king"}, description="Positive base word (A)" + ) + word_b: str = Field( + ..., json_schema_extra={"example": "man"}, description="Subtracted word (B)" + ) + word_c: str = Field( + ..., json_schema_extra={"example": "woman"}, description="Added word (C)" + ) + top_k: int = Field( + default=10, ge=1, le=100, description="Number of nearest neighbors to return" + ) + + +class CompareRequest(BaseModel): + texts: list[str] = Field( + ..., + description="List of texts/tokens to compute batch embeddings for (1 to 1024 items)", + ) + + +class ProjectParams(BaseModel): + n_neighbors: int = Field(default=15, ge=2, le=200) + min_dist: float = Field(default=0.1, ge=0.0) + metric: str = Field(default="cosine") + + +class ProjectRequest(BaseModel): + vectors: list[list[float]] = Field( + ..., + description="Precomputed embedding rows (1..1024); does not re-encode text", + ) + method: str = Field( + default="umap", + description="Projection method; v1 accepts 'umap' only", + ) + n_components: Literal[2, 3] = Field( + default=3, + description="Output dimensionality (Galaxy uses 3; 2 reserved for future 2D VIEW)", + ) + seed: int = Field(default=42, description="RNG seed for reproducibility") + params: ProjectParams | None = Field( + default=None, + description="UMAP hyperparameters (n_neighbors, min_dist, metric)", + ) + + +@router.get("/health") +def health_check() -> dict[str, Any]: + selection = getattr(state, "selection", None) + short_label = getattr(selection, "short_label", None) if selection else None + return { + "status": "ok" if state.is_loaded else "uninitialized", + "model": state.model_name, + "model_profile": getattr(state, "model_profile", None), + "short_label": short_label, + "embedding_dim": getattr(state, "embedding_dim", None), + "truncate_dim": getattr(state, "truncate_dim", None), + "vocab_size": len(state.vocab_words), + "is_loaded": state.is_loaded, + "device": getattr(state, "device", "cpu"), + } + + +@router.post("/embed") +def embed_text(req: EmbedRequest) -> dict[str, Any]: + if not state.is_loaded or state.model is None: + raise HTTPException(status_code=503, detail="Backend model is not loaded yet") + if not req.text.strip(): + raise HTTPException(status_code=400, detail="Text cannot be empty") + + vec = state.compute_embedding(req.text) + return {"text": req.text, "embedding": vec.tolist(), "dimension": len(vec)} + + +@router.post("/tokenize") +def tokenize_text(req: TokenizeRequest) -> dict[str, Any]: + if not state.is_loaded or state.model is None: + raise HTTPException(status_code=503, detail="Backend model is not loaded yet") + + # SentenceTransformer uses underlying Hugging Face tokenizer + tokenizer = getattr(state.model, "tokenizer", None) + if tokenizer is not None: + tokens = tokenizer.tokenize(req.text) + input_ids = tokenizer.encode(req.text) + else: + # Fallback space tokenization + tokens = req.text.split() + input_ids = list(range(len(tokens))) + + return { + "text": req.text, + "tokens": tokens, + "input_ids": input_ids, + "count": len(tokens), + } + + +@router.post("/arithmetic") +def perform_arithmetic(req: ArithmeticRequest) -> dict[str, Any]: + if not state.is_loaded or state.model is None: + raise HTTPException(status_code=503, detail="Backend model is not loaded yet") + + if not (req.word_a.strip() and req.word_b.strip() and req.word_c.strip()): + raise HTTPException( + status_code=400, detail="Words A, B, and C must not be empty" + ) + + try: + res = state.perform_arithmetic(req.word_a, req.word_b, req.word_c, req.top_k) + return res + except Exception as e: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/compare") +def perform_compare(req: CompareRequest) -> dict[str, Any]: + if not state.is_loaded or state.model is None: + raise HTTPException(status_code=503, detail="Backend model is not loaded yet") + + if not req.texts: + raise HTTPException(status_code=400, detail="Texts list cannot be empty") + + try: + return state.perform_compare(req.texts) + except Exception as e: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/project") +def perform_project(req: ProjectRequest) -> dict[str, Any]: + """Project embedding vectors to 2D/3D (UMAP). Does not encode text.""" + params = req.params.model_dump() if req.params is not None else None + try: + return project_embeddings( + req.vectors, + method=req.method, + n_components=req.n_components, + seed=req.seed, + params=params, + ) + except ProjectError as e: + raise HTTPException(status_code=e.status_code, detail=e.message) + except Exception as e: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(e)) + diff --git a/backend/routers/sae.py b/backend/routers/sae.py new file mode 100644 index 0000000000000000000000000000000000000000..8b107558678cd7977a790c1e01d31256c903426e --- /dev/null +++ b/backend/routers/sae.py @@ -0,0 +1,400 @@ +"""SAE API: status / train / encode / clear — persisted Compare-scope Top-K SAE.""" + +from __future__ import annotations + +import asyncio +import logging +import os +from pathlib import Path +from typing import Any + +import numpy as np +import orjson +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel, Field +from starlette.responses import JSONResponse + +from backend.sae.sae_model import SAEManager +from backend.sae.suggest_dims import suggest_sae_dims, suggest_train_schedule +from backend.sae.train_sae import get_optimal_device, train_sae + +logger = logging.getLogger(__name__) + + +class OrjsonResponse(JSONResponse): + """FastJSON via orjson (avoid deprecated fastapi.responses.ORJSONResponse).""" + + media_type = "application/json" + + def render(self, content: Any) -> bytes: + return orjson.dumps(content) + + +router = APIRouter(default_response_class=OrjsonResponse) + +_ARTIFACTS_DIR = Path(__file__).resolve().parent.parent / "artifacts" +SAE_WEIGHTS_PATH = str(_ARTIFACTS_DIR / "sae_weights.pt") + + +def _resolve_sae_device() -> str: + return get_optimal_device(os.getenv("SAE_DEVICE", "AUTO")) + + +sae_manager = SAEManager( + device=_resolve_sae_device(), + weights_path=SAE_WEIGHTS_PATH, +) + + +class SAEEncodeRequest(BaseModel): + embeddings: list[list[float]] = Field( + ..., description="Batch of raw embedding vectors [N, D]" + ) + + +class SAETrainRequest(BaseModel): + embeddings: list[list[float]] = Field( + ..., + min_length=2, + description="Current workspace scope vectors (Compare items or Arithmetic components)", + ) + hidden_dim: int = Field(default=8192, ge=32, le=32768) + k: int = Field(default=32, ge=1, le=4096) + epochs: int = Field(default=50, ge=1, le=500) + lr: float = Field(default=1e-3, gt=0, le=1.0) + batch_size: int = Field(default=64, ge=1, le=2048) + auto_scale: bool = Field( + default=True, + description="Scale hidden_dim/k down for small N (recommended)", + ) + + +class SAETrainingStatus: + def __init__(self) -> None: + self.status = "idle" # idle | preparing | training | success | failed + self.phase = "idle" + self.phase_key = "idle" # idle | preparing | training | installing | ready | failed + self.current_epoch = 0 + self.total_epochs = 0 + self.remaining_epochs = 0 + self.percent = 0.0 + self.loss = 0.0 + self.error_message: str | None = None + self.metrics: dict[str, Any] | None = None + self.message = "" + self.resolved_hidden: int | None = None + self.resolved_k: int | None = None + self.n_vectors: int | None = None + + +sae_training_status = SAETrainingStatus() + + +async def _train_sae_task( + vectors: np.ndarray, + hidden_dim: int, + k: int, + epochs: int, + lr: float, + batch_size: int, +) -> None: + global sae_training_status + try: + n = int(vectors.shape[0]) + sae_training_status.status = "training" + sae_training_status.phase_key = "preparing" + sae_training_status.phase = "Preparing scope matrix…" + sae_training_status.current_epoch = 0 + sae_training_status.total_epochs = epochs + sae_training_status.remaining_epochs = epochs + sae_training_status.percent = 0.0 + sae_training_status.loss = 0.0 + sae_training_status.error_message = None + sae_training_status.metrics = None + sae_training_status.message = ( + f"Preparing scope matrix… (n={n}, hidden={hidden_dim}, k={k}, epochs={epochs})" + ) + sae_training_status.resolved_hidden = hidden_dim + sae_training_status.resolved_k = k + sae_training_status.n_vectors = n + + # Drop previous in-RAM model (file replaced on install) + sae_manager.unload() + + def progress_cb( + completed: float, + total_epochs: int, + loss: float, + detail: str = "", + ) -> None: + completed_f = float(completed) + total_epochs = int(total_epochs) + completed_i = int(completed_f) if completed_f < total_epochs else total_epochs + remaining = max(0, total_epochs - completed_i) + # Keep a fractional "still in this epoch" remaining feel + if completed_f < total_epochs and completed_f > completed_i: + remaining = max(0, total_epochs - completed_i - 1) + pct = ( + round(100.0 * min(completed_f, float(total_epochs)) / total_epochs, 1) + if total_epochs + else 0.0 + ) + sae_training_status.current_epoch = completed_i + sae_training_status.total_epochs = total_epochs + sae_training_status.remaining_epochs = remaining + sae_training_status.percent = min(99.0, pct) if completed_f < total_epochs else pct + sae_training_status.loss = float(loss) + sae_training_status.phase_key = "training" + sae_training_status.status = "training" + if detail: + sae_training_status.phase = detail + elif completed_f < total_epochs: + running = min(total_epochs, completed_i + 1) + loss_bit = f" · loss={loss:.6f}" if loss else "" + sae_training_status.phase = ( + f"Training epoch {running}/{total_epochs} — " + f"{remaining} remaining{loss_bit}" + ) + else: + sae_training_status.phase = ( + f"Epochs done {total_epochs}/{total_epochs} · loss={loss:.6f}" + ) + sae_training_status.message = sae_training_status.phase + + sae_training_status.phase_key = "training" + sae_training_status.phase = ( + f"Training epoch 1/{epochs} — {epochs} remaining" + ) + sae_training_status.message = sae_training_status.phase + metrics, checkpoint = await asyncio.to_thread( + train_sae, + vectors=vectors, + hidden_dim=hidden_dim, + k=k, + epochs=epochs, + lr=lr, + batch_size=batch_size, + device_name=os.getenv("SAE_DEVICE", "AUTO"), + output_path=None, + progress_cb=progress_cb, + ) + + sae_training_status.phase_key = "installing" + sae_training_status.current_epoch = epochs + sae_training_status.remaining_epochs = 0 + sae_training_status.percent = 99.0 + sae_training_status.phase = "Saving checkpoint…" + sae_training_status.message = ( + f"Saving checkpoint… ({hidden_dim}D · k={k} · n={n})" + ) + sae_manager.device = _resolve_sae_device() + sae_manager.install_checkpoint(checkpoint, persist=True) + + sae_training_status.metrics = metrics + sae_training_status.status = "success" + sae_training_status.phase_key = "ready" + sae_training_status.percent = 100.0 + sae_training_status.remaining_epochs = 0 + sae_training_status.phase = "Ready" + sae_training_status.message = ( + f"Ready — saved SAE {hidden_dim}D · k={k} · n={n}" + ) + logger.info("SAE training completed (checkpoint saved).") + + except Exception as e: + logger.exception("SAE training failed") + sae_manager.clear(delete_file=False) + sae_training_status.status = "failed" + sae_training_status.phase_key = "failed" + sae_training_status.phase = "failed" + sae_training_status.error_message = str(e) + sae_training_status.message = str(e) + + +@router.get("/sae/status") +async def sae_status() -> dict[str, Any]: + is_trained = sae_manager.is_trained() + status_info: dict[str, Any] = { + "is_trained": is_trained, + "ephemeral": False, + "persisted": sae_manager.has_checkpoint_file(), + "weights_path": SAE_WEIGHTS_PATH, + "config": None, + "metrics": None, + "training": { + "status": sae_training_status.status, + "phase": sae_training_status.phase, + "phase_key": sae_training_status.phase_key, + "message": sae_training_status.message, + "current_epoch": sae_training_status.current_epoch, + "total_epochs": sae_training_status.total_epochs, + "remaining_epochs": sae_training_status.remaining_epochs, + "percent": sae_training_status.percent, + "loss": sae_training_status.loss, + "error_message": sae_training_status.error_message, + "metrics": sae_training_status.metrics, + "resolved_hidden": sae_training_status.resolved_hidden, + "resolved_k": sae_training_status.resolved_k, + "n_vectors": sae_training_status.n_vectors, + }, + } + if is_trained: + if sae_manager.load_model(): + status_info["config"] = sae_manager.config + status_info["metrics"] = sae_manager.metrics + return status_info + + +@router.post("/sae/clear") +async def sae_clear() -> dict[str, Any]: + """Delete the saved SAE checkpoint and drop RAM weights (Retrain confirm).""" + if sae_training_status.status == "training": + raise HTTPException( + status_code=400, + detail="Cannot clear SAE while a training job is in progress.", + ) + sae_manager.clear(delete_file=True) + sae_training_status.status = "idle" + sae_training_status.phase = "idle" + sae_training_status.phase_key = "idle" + sae_training_status.message = "" + sae_training_status.metrics = None + sae_training_status.error_message = None + sae_training_status.resolved_hidden = None + sae_training_status.resolved_k = None + sae_training_status.n_vectors = None + sae_training_status.current_epoch = 0 + sae_training_status.total_epochs = 0 + sae_training_status.remaining_epochs = 0 + sae_training_status.percent = 0.0 + return {"message": "SAE checkpoint deleted.", "is_trained": False} + + +@router.post("/sae/train") +async def sae_train(request: SAETrainRequest) -> dict[str, Any]: + if sae_training_status.status == "training": + raise HTTPException( + status_code=400, detail="A training job is already in progress." + ) + + vectors = np.asarray(request.embeddings, dtype=np.float32) + if vectors.ndim != 2: + raise HTTPException( + status_code=400, detail="embeddings must be a 2D array [N, D]" + ) + if vectors.shape[0] < 2: + raise HTTPException( + status_code=400, detail="Need at least 2 embedding vectors to train SAE" + ) + + input_dim = int(vectors.shape[1]) + n_vectors = int(vectors.shape[0]) + if request.auto_scale: + hidden_dim, k = suggest_sae_dims( + n_vectors=n_vectors, + input_dim=input_dim, + requested_hidden=request.hidden_dim, + requested_k=request.k, + ) + epochs, batch_size = suggest_train_schedule( + n_vectors=n_vectors, + hidden_dim=hidden_dim, + requested_epochs=request.epochs, + requested_batch_size=request.batch_size, + ) + else: + hidden_dim, k = request.hidden_dim, request.k + epochs, batch_size = request.epochs, request.batch_size + + if k > hidden_dim: + raise HTTPException(status_code=400, detail="k must be <= hidden_dim") + + asyncio.create_task( + _train_sae_task( + vectors=vectors, + hidden_dim=hidden_dim, + k=k, + epochs=epochs, + lr=request.lr, + batch_size=batch_size, + ) + ) + return { + "message": "Training started.", + "status": "training", + "resolved_hidden": hidden_dim, + "resolved_k": k, + "resolved_epochs": epochs, + "resolved_batch_size": batch_size, + "n_vectors": n_vectors, + "device": _resolve_sae_device(), + "auto_scaled": bool( + request.auto_scale + and ( + hidden_dim != request.hidden_dim + or k != request.k + or epochs != request.epochs + or batch_size != request.batch_size + ) + ), + } + + +@router.post("/sae/encode") +async def sae_encode(request: SAEEncodeRequest) -> dict[str, Any]: + """ + Encode embeddings → Top-K sparse activations (indices + values). + + Does NOT return a dense [N, hidden_dim] matrix (mostly zeros). Clients densify + locally when needed. Model stays resident in RAM after first load_model(). + """ + if not sae_manager.is_trained(): + raise HTTPException( + status_code=404, + detail="No trained SAE model available. Please train one first.", + ) + + try: + if not request.embeddings: + # Ensure config is in RAM if only the checkpoint file exists + sae_manager.load_model() + hidden = int(sae_manager.config.get("hidden_dim", 8192)) + k = int(sae_manager.config.get("k", 32)) + return { + "format": "topk_sparse", + "indices": [], + "values": [], + "dimension": hidden, + "k": k, + "count": 0, + "batch_metrics": { + "l0": 0.0, + "sparsity": 0.0, + "active_features": 0, + }, + } + + vectors = np.asarray(request.embeddings, dtype=np.float32) + if vectors.ndim != 2: + raise HTTPException( + status_code=400, detail="embeddings must be a 2D array [N, D]" + ) + + sparse = await asyncio.to_thread(sae_manager.encode_vectors_sparse, vectors) + # ORJSON serializes ndarray; convert only the small [N, K] packs + return { + "format": sparse["format"], + "indices": sparse["indices"].tolist(), + "values": sparse["values"].tolist(), + "dimension": int(sparse["dimension"]), + "k": int(sparse["k"]), + "count": int(sparse["count"]), + "batch_metrics": sparse["batch_metrics"], + } + except HTTPException: + raise + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) from e + except Exception: + logger.exception("Error encoding embeddings through SAE") + raise HTTPException(status_code=500, detail="SAE encode failed") from None diff --git a/backend/sae/__init__.py b/backend/sae/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9b9ed1746e4e4b22e62c0f264043dd327b5abd7a --- /dev/null +++ b/backend/sae/__init__.py @@ -0,0 +1,14 @@ +"""Top-K Sparse Autoencoder (trained) for Clean/Denoise.""" + +from backend.sae.sae_model import SAEManager, TopKSAE +from backend.sae.suggest_dims import suggest_sae_dims, suggest_train_schedule +from backend.sae.train_sae import get_optimal_device, train_sae + +__all__ = [ + "TopKSAE", + "SAEManager", + "train_sae", + "get_optimal_device", + "suggest_sae_dims", + "suggest_train_schedule", +] diff --git a/backend/sae/sae_model.py b/backend/sae/sae_model.py new file mode 100644 index 0000000000000000000000000000000000000000..ce2e29bd396df92827acb8ac9e0d809c52a488b6 --- /dev/null +++ b/backend/sae/sae_model.py @@ -0,0 +1,326 @@ +"""Top-K SAE model and lazy-load manager (ported from predecessor tool).""" + +from __future__ import annotations + +import logging +import os +from typing import Any + +import numpy as np +import torch +from torch import nn + +logger = logging.getLogger("sae_model") + + +class TopKSAE(nn.Module): + """ + Sparse Autoencoder with Top-K activation sparsity. + Projects D-dimensional embeddings to a higher-dimensional sparse space. + """ + + def __init__(self, input_dim: int = 768, hidden_dim: int = 8192, k: int = 32): + super().__init__() + self.input_dim = input_dim + self.hidden_dim = hidden_dim + self.k = k + + self.b_dec = nn.Parameter(torch.zeros(input_dim)) + self.W_enc = nn.Parameter(torch.empty(input_dim, hidden_dim)) + self.b_enc = nn.Parameter(torch.zeros(hidden_dim)) + self.W_dec = nn.Parameter(torch.empty(hidden_dim, input_dim)) + + self.reset_parameters() + + def reset_parameters(self) -> None: + nn.init.kaiming_uniform_(self.W_enc, nonlinearity="relu") + nn.init.kaiming_uniform_(self.W_dec, nonlinearity="linear") + self.make_decoder_weights_unit_norm() + + def make_decoder_weights_unit_norm(self) -> None: + """Constrains decoder weight rows (W_dec) to unit L2 norm.""" + with torch.no_grad(): + self.W_dec.data.div_( + torch.norm(self.W_dec.data, dim=1, keepdim=True) + 1e-8 + ) + + def encode(self, x: torch.Tensor) -> torch.Tensor: + x_centered = x - self.b_dec + pre_acts = torch.relu(x_centered @ self.W_enc + self.b_enc) + + # Top-K in FP32 for dead-latent / overflow stability + if self.k < self.hidden_dim: + pre_acts_fp32 = pre_acts.float() + topk_vals, topk_indices = torch.topk(pre_acts_fp32, self.k, dim=-1) + acts = torch.zeros_like(pre_acts_fp32).scatter_(-1, topk_indices, topk_vals) + if acts.dtype != pre_acts.dtype: + acts = acts.to(pre_acts.dtype) + else: + acts = pre_acts + + return acts + + def decode(self, acts: torch.Tensor) -> torch.Tensor: + if acts.dtype != self.W_dec.dtype: + acts = acts.to(self.W_dec.dtype) + return acts @ self.W_dec + self.b_dec + + def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + acts = self.encode(x) + x_reconstructed = self.decode(acts) + return x_reconstructed, acts + + +class SAEManager: + """Lazy-load checkpoint from disk + in-memory encode for Clean/Denoise.""" + + def __init__(self, device: str = "cpu", weights_path: str | None = None): + self.weights_path = weights_path + self.device = device + self.model: TopKSAE | None = None + self.config: dict[str, Any] = {} + self.metrics: dict[str, Any] = {} + self.static_buckets = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024] + + def has_checkpoint_file(self) -> bool: + return bool(self.weights_path) and os.path.exists(self.weights_path) + + def is_trained(self) -> bool: + return self.model is not None or self.has_checkpoint_file() + + def clear(self, *, delete_file: bool = False) -> None: + """Drop in-memory model; optionally delete the saved checkpoint.""" + self.model = None + self.config = {} + self.metrics = {} + if delete_file and self.weights_path and os.path.exists(self.weights_path): + try: + os.remove(self.weights_path) + logger.info("Deleted SAE checkpoint %s", self.weights_path) + except OSError as e: + logger.warning("Could not delete SAE checkpoint: %s", e) + + def clear_if_dim_mismatch(self, embedding_dim: int) -> bool: + """ + Clear SAE RAM (+ delete checkpoint) when stored input_dim ≠ embedding_dim (M10). + + Returns True if something was cleared. No-op when SAE is empty or dims match. + """ + stored: int | None = None + if self.config.get("input_dim") is not None: + stored = int(self.config["input_dim"]) + elif self.model is not None: + stored = int(self.model.input_dim) + elif self.has_checkpoint_file(): + try: + checkpoint = torch.load(self.weights_path, map_location="cpu") + cfg = checkpoint.get("config") or {} + if cfg.get("input_dim") is not None: + stored = int(cfg["input_dim"]) + except Exception as e: # noqa: BLE001 + logger.warning( + "Could not read SAE checkpoint dim (%s); clearing to be safe.", e + ) + self.clear(delete_file=True) + return True + + if stored is None: + return False + if stored == int(embedding_dim): + return False + logger.warning( + "SAE input_dim=%s != embedding_dim=%s — clearing session SAE.", + stored, + embedding_dim, + ) + self.clear(delete_file=True) + return True + + def unload(self) -> None: + """Drop RAM only (keep file on disk).""" + self.model = None + self.config = {} + self.metrics = {} + + def save_checkpoint(self, checkpoint: dict[str, Any]) -> None: + if not self.weights_path: + return + parent = os.path.dirname(self.weights_path) + if parent: + os.makedirs(parent, exist_ok=True) + torch.save(checkpoint, self.weights_path) + logger.info("Saved SAE checkpoint to %s", self.weights_path) + + def install_checkpoint(self, checkpoint: dict[str, Any], *, persist: bool = True) -> None: + """Install a trained checkpoint into RAM; optionally persist to disk.""" + self.config = dict( + checkpoint.get("config", {"input_dim": 768, "hidden_dim": 8192, "k": 32}) + ) + self.metrics = dict(checkpoint.get("metrics", {})) + self.model = TopKSAE( + input_dim=int(self.config.get("input_dim", 768)), + hidden_dim=int(self.config.get("hidden_dim", 8192)), + k=int(self.config.get("k", 32)), + ) + self.model.load_state_dict(checkpoint["state_dict"]) + self.model.to(self.device) + self.model.eval() + if persist: + self.save_checkpoint(checkpoint) + logger.info( + "SAE installed (hidden_dim=%s, k=%s, persist=%s).", + self.config.get("hidden_dim"), + self.config.get("k"), + persist, + ) + + def load_model(self) -> bool: + if self.model is not None: + return True + + if not self.has_checkpoint_file(): + logger.warning("No SAE checkpoint available") + return False + + try: + logger.info( + "Loading SAE model from %s on %s...", self.weights_path, self.device + ) + checkpoint = torch.load(self.weights_path, map_location=self.device) + self.install_checkpoint(checkpoint, persist=False) + return True + except Exception as e: + logger.error("Error loading SAE model: %s", e) + self.model = None + return False + + def _encode_acts_tensor(self, vectors: np.ndarray) -> torch.Tensor: + """ + Run Top-K encode on [N, D] → device tensor [N, hidden_dim]. + Shares bucketing / from_numpy / inference_mode / AMP across dense + sparse paths. + Model must already be loaded (singleton in RAM/VRAM). + """ + assert self.model is not None + orig_len = vectors.shape[0] + if orig_len == 0: + return torch.empty( + (0, self.model.hidden_dim), + dtype=torch.float32, + device=self.device, + ) + + bucket_len = min( + (b for b in self.static_buckets if b >= orig_len), default=orig_len + ) + padding_needed = bucket_len - orig_len + + if padding_needed > 0: + padded_vectors = np.pad( + vectors, ((0, padding_needed), (0, 0)), mode="constant" + ) + else: + padded_vectors = vectors + + if not padded_vectors.flags.writeable: + padded_vectors = padded_vectors.copy() + x_tensor = torch.from_numpy(padded_vectors).to(self.device) + if x_tensor.dtype != torch.float32: + x_tensor = x_tensor.float() + + device_type = ( + "cuda" + if "cuda" in str(self.device) + else ("mps" if "mps" in str(self.device) else "cpu") + ) + use_amp = device_type in ["cuda", "cpu", "mps"] + amp_dtype = ( + torch.bfloat16 + if (device_type == "cuda" and torch.cuda.is_bf16_supported()) + else torch.float16 + ) + + with torch.inference_mode(): + if use_amp: + try: + with torch.amp.autocast(device_type=device_type, dtype=amp_dtype): + acts = self.model.encode(x_tensor) + except Exception: + acts = self.model.encode(x_tensor) + else: + acts = self.model.encode(x_tensor) + + return acts[:orig_len].float() + + def encode_vectors(self, vectors: np.ndarray) -> np.ndarray: + """ + Encode [N, D] embeddings → [N, hidden_dim] sparse activations (dense layout). + Prefer encode_vectors_sparse for HTTP — avoids shipping ~99% zeros. + """ + if not self.load_model() or self.model is None: + raise ValueError("SAE model is not trained or could not be loaded.") + + if vectors.shape[0] == 0: + return np.empty((0, self.model.hidden_dim), dtype=np.float32) + + return self._encode_acts_tensor(vectors).cpu().numpy() + + def encode_vectors_sparse(self, vectors: np.ndarray) -> dict[str, Any]: + """ + Encode [N, D] → Top-K sparse payload (indices/values only). + + Returns dict with numpy arrays ready for ORJSON (no dense [N, H] .tolist()). + """ + if not self.load_model() or self.model is None: + raise ValueError("SAE model is not trained or could not be loaded.") + + hidden = int(self.model.hidden_dim) + k = int(self.model.k) + orig_len = int(vectors.shape[0]) + if orig_len == 0: + return { + "format": "topk_sparse", + "indices": np.empty((0, k), dtype=np.int64), + "values": np.empty((0, k), dtype=np.float32), + "dimension": hidden, + "k": k, + "count": 0, + "batch_metrics": { + "l0": 0.0, + "sparsity": 0.0, + "active_features": 0, + }, + } + + with torch.inference_mode(): + acts = self._encode_acts_tensor(vectors) + # acts already Top-K sparse in dense layout; pack the K slots + topk_k = min(k, hidden) + vals, idx = torch.topk(acts, topk_k, dim=-1) + vals_np = vals.cpu().numpy().astype(np.float32, copy=False) + idx_np = idx.cpu().numpy().astype(np.int64, copy=False) + + active_per_row = (vals > 0).sum(dim=-1).float() + l0 = float(active_per_row.mean().item()) if orig_len else 0.0 + if orig_len and topk_k > 0: + # Unique latent ids that fired at least once in the batch + pos_mask = vals > 0 + if bool(pos_mask.any().item()): + active_features = int(torch.unique(idx[pos_mask]).numel()) + else: + active_features = 0 + else: + active_features = 0 + + return { + "format": "topk_sparse", + "indices": idx_np, + "values": vals_np, + "dimension": hidden, + "k": topk_k, + "count": orig_len, + "batch_metrics": { + "l0": l0, + "sparsity": float(l0 / hidden) if hidden else 0.0, + "active_features": active_features, + }, + } diff --git a/backend/sae/suggest_dims.py b/backend/sae/suggest_dims.py new file mode 100644 index 0000000000000000000000000000000000000000..421badf6ed4c5311948963b3afc687cc19448d3d --- /dev/null +++ b/backend/sae/suggest_dims.py @@ -0,0 +1,86 @@ +"""Auto-scale Top-K SAE width + train schedule for small session scopes.""" + +from __future__ import annotations + + +def suggest_sae_dims( + n_vectors: int, + input_dim: int, + requested_hidden: int = 8192, + requested_k: int = 32, +) -> tuple[int, int]: + """ + Cap hidden_dim / k so a small Compare batch does not train an 8192-wide dictionary. + + Returns (hidden_dim, k) with hidden_dim <= requested_hidden and k <= min(requested_k, hidden_dim). + """ + if n_vectors < 2: + raise ValueError("Need at least 2 embedding vectors to train SAE") + if input_dim < 1: + raise ValueError("input_dim must be positive") + if requested_hidden < 16: + raise ValueError("requested_hidden must be >= 16") + if requested_k < 1: + raise ValueError("requested_k must be >= 1") + + # Keep dictionary proportional to sample count (aggressive for interactive UX). + if n_vectors < 32: + by_n = max(32, n_vectors * 3) + elif n_vectors < 64: + by_n = max(32, n_vectors * 4) + elif n_vectors < 256: + by_n = max(64, n_vectors * 5) + else: + by_n = max(128, n_vectors * 8) + + by_d = max(input_dim, min(requested_hidden, input_dim * 2)) + + if n_vectors >= max(512, requested_hidden // 4): + hidden = requested_hidden + else: + hidden = min(requested_hidden, max(32, min(by_n, by_d))) + + if hidden >= 32: + hidden = max(32, (hidden // 32) * 32) + hidden = max(16, min(int(hidden), requested_hidden)) + + k = min(requested_k, hidden) + if hidden >= 4: + k = min(k, max(1, hidden // 4)) + k = max(1, int(k)) + return hidden, k + + +def suggest_train_schedule( + n_vectors: int, + hidden_dim: int, + requested_epochs: int = 50, + requested_batch_size: int = 64, +) -> tuple[int, int]: + """ + Cap epochs / prefer full-batch for interactive session SAE training. + + Returns (epochs, batch_size). + """ + if n_vectors < 2: + raise ValueError("Need at least 2 embedding vectors to train SAE") + + # Smaller dictionaries converge in fewer passes; keep UX snappy. + if hidden_dim <= 128: + epochs = min(requested_epochs, 12) + elif hidden_dim <= 512: + epochs = min(requested_epochs, 20) + elif n_vectors < 64: + epochs = min(requested_epochs, 25) + else: + epochs = requested_epochs + epochs = max(1, int(epochs)) + + # Full-batch when N fits — avoids DataLoader overhead on small scopes. + if n_vectors <= 512: + batch_size = n_vectors + else: + batch_size = min(int(requested_batch_size), n_vectors) + batch_size = max(8, batch_size) + + return epochs, batch_size diff --git a/backend/sae/train_sae.py b/backend/sae/train_sae.py new file mode 100644 index 0000000000000000000000000000000000000000..3ac5dd3f54ebcc60e4d6b86aa3b9dcb191165e37 --- /dev/null +++ b/backend/sae/train_sae.py @@ -0,0 +1,326 @@ +"""Train Top-K SAE on a session-scope embedding matrix (fast path).""" + +from __future__ import annotations + +import logging +import os +from collections.abc import Callable +from typing import Any + +import numpy as np +import torch +import torch.nn as nn +import torch.optim as optim +from torch.utils.data import DataLoader, TensorDataset + +from backend.device import get_optimal_device +from backend.sae.sae_model import TopKSAE + +logger = logging.getLogger("train_sae") + +ProgressCb = Callable[..., None] + +# Re-export for callers that imported get_optimal_device from this module. +__all__ = ["get_optimal_device", "train_sae"] + + +def _device_type(device: torch.device | str) -> str: + s = str(device) + if "cuda" in s: + return "cuda" + if "mps" in s: + return "mps" + return "cpu" + + +def _amp_settings(device: torch.device) -> tuple[bool, torch.dtype | None]: + """Mixed precision for CUDA matmuls. MPS/CPU train stays FP32 for stability.""" + if device.type == "cuda": + dtype = ( + torch.bfloat16 + if torch.cuda.is_bf16_supported() + else torch.float16 + ) + return True, dtype + return False, None + + +def _emit_progress( + progress_cb: ProgressCb | None, + completed: float, + total_epochs: int, + loss: float, + detail: str = "", +) -> None: + if progress_cb is None: + return + try: + if detail: + progress_cb(completed, total_epochs, float(loss), detail) + else: + progress_cb(completed, total_epochs, float(loss)) + except TypeError: + try: + progress_cb(completed, total_epochs, float(loss)) + except Exception: + pass + except Exception: + pass + + +def train_sae( + vectors: np.ndarray, + hidden_dim: int = 8192, + k: int = 32, + epochs: int = 50, + lr: float = 1e-3, + batch_size: int = 64, + device_name: str = "AUTO", + output_path: str | None = None, + progress_cb: ProgressCb | None = None, +) -> tuple[dict[str, Any], dict[str, Any]]: + """ + Train TopKSAE with Adam + MSE reconstruction (session-scope fast path). + + Optimizations vs naive loop: + - contiguous float32 + torch.from_numpy (zero-copy into torch) + - full-batch when N <= batch_size (no DataLoader overhead) + - CUDA/CPU AMP autocast on forward + - inference_mode for final metrics + - decoder unit-norm every step (small H) / every 2 steps (large H) + + Returns (metrics, checkpoint). + """ + if vectors.ndim != 2 or vectors.shape[0] == 0: + raise ValueError("vectors must be a non-empty 2D array [N, D]") + + device = torch.device(get_optimal_device(device_name)) + logger.info("Training on device: %s", device) + + # Zero-copy friendly contiguous float32 + vectors = np.ascontiguousarray(vectors, dtype=np.float32) + input_dim = int(vectors.shape[1]) + n = int(vectors.shape[0]) + + model = TopKSAE(input_dim=input_dim, hidden_dim=hidden_dim, k=k) + model.to(device) + + x_host = torch.from_numpy(vectors) # shares memory with NumPy + actual_batch_size = min(max(1, batch_size), n) + use_full_batch = actual_batch_size >= n + + if use_full_batch: + x_all = x_host.to(device, non_blocking=True) + dataloader = None + else: + dataset = TensorDataset(x_host) + dataloader = DataLoader( + dataset, + batch_size=actual_batch_size, + shuffle=True, + drop_last=False, + pin_memory=(device.type == "cuda"), + ) + x_all = None + + optimizer = optim.Adam(model.parameters(), lr=lr) + use_amp, amp_dtype = _amp_settings(device) + # GradScaler only for cuda fp16 (not bf16 / not mps) + scaler = ( + torch.amp.GradScaler("cuda") + if (use_amp and _device_type(device) == "cuda" and amp_dtype == torch.float16) + else None + ) + norm_every = 1 if hidden_dim <= 1024 else 2 + step_i = 0 + + logger.info( + "Starting SAE training (n=%s, hidden_dim=%s, k=%s, epochs=%s, batch_size=%s, full_batch=%s, amp=%s)...", + n, + hidden_dim, + k, + epochs, + actual_batch_size, + use_full_batch, + use_amp, + ) + + last_loss = 0.0 + for epoch in range(epochs): + model.train() + total_loss = 0.0 + + if use_full_batch: + assert x_all is not None + optimizer.zero_grad(set_to_none=True) + if use_amp and amp_dtype is not None: + with torch.amp.autocast(device_type=_device_type(device), dtype=amp_dtype): + reconstruction, _acts = model(x_all) + loss = nn.functional.mse_loss(reconstruction, x_all) + else: + reconstruction, _acts = model(x_all) + loss = nn.functional.mse_loss(reconstruction, x_all) + + if scaler is not None: + scaler.scale(loss).backward() + scaler.step(optimizer) + scaler.update() + else: + loss.backward() + optimizer.step() + + step_i += 1 + if step_i % norm_every == 0: + model.make_decoder_weights_unit_norm() + + last_loss = float(loss.detach().item()) + total_loss = last_loss * n + _emit_progress( + progress_cb, + float(epoch) + 1.0, + epochs, + last_loss, + f"Training epoch {epoch + 1}/{epochs} (full-batch) — " + f"{max(0, epochs - epoch - 1)} left · loss={last_loss:.6f}", + ) + else: + assert dataloader is not None + n_batches = max(1, len(dataloader)) + for batch_i, batch in enumerate(dataloader): + x_batch = batch[0].to(device, non_blocking=True) + optimizer.zero_grad(set_to_none=True) + if use_amp and amp_dtype is not None: + with torch.amp.autocast( + device_type=_device_type(device), dtype=amp_dtype + ): + reconstruction, _acts = model(x_batch) + loss = nn.functional.mse_loss(reconstruction, x_batch) + else: + reconstruction, _acts = model(x_batch) + loss = nn.functional.mse_loss(reconstruction, x_batch) + + if scaler is not None: + scaler.scale(loss).backward() + scaler.step(optimizer) + scaler.update() + else: + loss.backward() + optimizer.step() + + step_i += 1 + if step_i % norm_every == 0: + model.make_decoder_weights_unit_norm() + + batch_loss = float(loss.detach().item()) + total_loss += batch_loss * len(x_batch) + + report = ( + batch_i == 0 + or batch_i == n_batches - 1 + or batch_i % max(1, n_batches // 10) == 0 + ) + if report: + frac = float(epoch) + float(batch_i + 1) / float(n_batches) + _emit_progress( + progress_cb, + frac, + epochs, + batch_loss, + f"Training epoch {epoch + 1}/{epochs} · " + f"batch {batch_i + 1}/{n_batches} — " + f"{max(0, epochs - epoch - 1)} epochs left after this", + ) + + last_loss = total_loss / n + _emit_progress( + progress_cb, + float(epoch + 1), + epochs, + last_loss, + f"Finished epoch {epoch + 1}/{epochs} · loss={last_loss:.6f}", + ) + + if (epoch + 1) % max(1, epochs // 10) == 0 or epoch == epochs - 1: + logger.info( + "Epoch %03d/%03d - Reconstruction Loss (MSE): %.6f", + epoch + 1, + epochs, + last_loss, + ) + + # Ensure unit-norm after last step + model.make_decoder_weights_unit_norm() + model.eval() + logger.info("Computing final training metrics...") + + with torch.inference_mode(): + if x_all is None: + x_eval = x_host.to(device, non_blocking=True) + else: + x_eval = x_all + if use_amp and amp_dtype is not None and _device_type(device) != "mps": + with torch.amp.autocast(device_type=_device_type(device), dtype=amp_dtype): + reconstruction, acts = model(x_eval) + else: + reconstruction, acts = model(x_eval) + + # Metrics in FP32 for stability + reconstruction = reconstruction.float() + acts = acts.float() + x_eval_f = x_eval.float() + final_mse = nn.functional.mse_loss(reconstruction, x_eval_f).item() + active_per_input = (acts > 0).sum(dim=-1).float() + mean_sparsity = active_per_input.mean().item() + feature_activations = (acts > 0).sum(dim=0) + dead_features_count = int((feature_activations == 0).sum().item()) + dead_features_pct = (dead_features_count / hidden_dim) * 100.0 + + logger.info("Training Complete.") + logger.info("Final Reconstruction Loss (MSE): %.6f", final_mse) + logger.info( + "Mean Sparsity (Active Features): %.2f / %s (Target K: %s)", + mean_sparsity, + hidden_dim, + k, + ) + logger.info( + "Dead Features: %s/%s (%.2f%%)", + dead_features_count, + hidden_dim, + dead_features_pct, + ) + + config = { + "input_dim": input_dim, + "hidden_dim": hidden_dim, + "k": k, + "device": str(device), + } + metrics = { + "mse": float(final_mse), + "mean_sparsity": float(mean_sparsity), + "dead_features_count": int(dead_features_count), + "dead_features_pct": float(dead_features_pct), + "total_vectors": n, + "epochs": int(epochs), + "batch_size": int(actual_batch_size), + "full_batch": bool(use_full_batch), + "amp": bool(use_amp), + "device": str(device), + } + + cpu_state = {k_: v.detach().cpu() for k_, v in model.state_dict().items()} + checkpoint: dict[str, Any] = { + "state_dict": cpu_state, + "config": config, + "metrics": metrics, + } + + if output_path: + parent = os.path.dirname(output_path) + if parent: + os.makedirs(parent, exist_ok=True) + torch.save(checkpoint, output_path) + logger.info("Saved SAE debug checkpoint to %s", output_path) + + return metrics, checkpoint diff --git a/backend/server.py b/backend/server.py new file mode 100644 index 0000000000000000000000000000000000000000..3bfbc77f360894559f6a5919050dd5dc30f45759 --- /dev/null +++ b/backend/server.py @@ -0,0 +1,111 @@ +""" +FastAPI Server Entrypoint for VHectorLab 3D. +Handles application lifespan, lazy-loading models, CORS configuration, and router binding. +""" + +import logging +import os +import sys +from contextlib import asynccontextmanager +from pathlib import Path + +# Ensure parent directory is in sys.path so 'backend.*' package imports resolve correctly +_root_dir = str(Path(__file__).resolve().parent.parent) +if _root_dir not in sys.path: + sys.path.insert(0, _root_dir) + +from backend.model_catalog import resolve_selection_from_env +from backend.model_swap import apply_dotenv +from backend.routers.core import router as core_router +from backend.routers.sae import router as sae_router +from backend.routers.sae import sae_manager +from backend.state import state +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +# Repo-root `.env` (setup.sh option 11 / option 1). `uv run` from backend/ does not +# load it automatically — without this, MODEL_* stay unset and default to mpnet. +apply_dotenv(Path(_root_dir) / ".env") + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("vhectorlab") + + +@asynccontextmanager +async def lifespan(app: FastAPI): + logger.info("Initializing VHectorLab 3D backend lifespan...") + vocab_path = os.getenv("VOCAB_PATH", "public/vocab.txt") + vocab_embeddings_path = os.getenv( + "VOCAB_EMBEDDINGS_PATH", "public/vocab_embeddings.npz" + ) + selection = resolve_selection_from_env() + + # Lazy load model and vocabulary into AppState + try: + state.load_model_and_vocab( + vocab_path=vocab_path, + vocab_embeddings_path=vocab_embeddings_path, + selection=selection, + ) + if state.embedding_dim is not None: + sae_manager.clear_if_dim_mismatch(state.embedding_dim) + except Exception as e: # noqa: BLE001 + logger.error(f"Error loading model/vocab in lifespan: {e}") + + yield + + logger.info("Shutting down VHectorLab 3D backend...") + + +app = FastAPI( + title="VHectorLab 3D API", + description="Backend API for 3D Vector Arithmetic & Semantic Embedding Visualizer", + version="3.0.1", + lifespan=lifespan, +) + +# CORS Policy Alignment: allow_origins=["*"] combined with allow_credentials=False +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(core_router, prefix="/api") +app.include_router(sae_router, prefix="/api") +app.include_router(core_router) # Also expose without /api prefix for convenience +app.include_router(sae_router) + +# Mount static frontend files if dist/ exists (Docker / Production mode) +from backend.static_dist import resolve_dist_file +from fastapi.responses import FileResponse +from fastapi.staticfiles import StaticFiles + +dist_path = Path(__file__).resolve().parent.parent / "dist" +if dist_path.exists(): + logger.info(f"Serving static frontend files from {dist_path}") + app.mount("/assets", StaticFiles(directory=dist_path / "assets"), name="assets") + + @app.get("/{full_path:path}") + async def serve_frontend(full_path: str): + return FileResponse(resolve_dist_file(dist_path, full_path)) + + +def _want_reload(host: str) -> bool: + """Local bare-metal defaults to reload; Docker/HF (0.0.0.0 or UVICORN_RELOAD=0) does not.""" + env = os.getenv("UVICORN_RELOAD") + if env is not None: + return env.strip().lower() in ("1", "true", "yes") + return host in ("127.0.0.1", "localhost") + + +if __name__ == "__main__": + import uvicorn + + host = os.getenv("HOST", "127.0.0.1") + port = int(os.getenv("PORT", "8000")) + reload = _want_reload(host) + logger.info("Starting uvicorn host=%s port=%s reload=%s", host, port, reload) + uvicorn.run("backend.server:app", host=host, port=port, reload=reload) diff --git a/backend/state.py b/backend/state.py new file mode 100644 index 0000000000000000000000000000000000000000..ac87178aecb203ba01b2aa97d0e750cdb6be3763 --- /dev/null +++ b/backend/state.py @@ -0,0 +1,340 @@ +""" +Global App State for VHectorLab 3D. +Manages lazy-loaded SentenceTransformer model and pre-computed vocabulary embeddings in RAM. +""" + +import logging +import os +from pathlib import Path +from typing import Any + +import numpy as np +from backend.model_catalog import ( + ModelSelection, + build_model, + encode_texts, + resolve_selection_from_env, +) +from backend.vocab_embeddings import ( + load_vocab_embeddings_npz as _load_vocab_cache, +) +from backend.vocab_embeddings import ( + npz_compatible_with_selection, + save_vocab_embeddings_npz, +) + +logger = logging.getLogger(__name__) + + +def _resolve_path(vocab_path: str) -> Path | None: + path = Path(vocab_path) + if path.exists(): + return path + alt_path = Path(__file__).resolve().parent.parent / vocab_path + if alt_path.exists(): + return alt_path + return None + + +def load_vocab_embeddings_npz( + npz_path: Path, +) -> tuple[list[str], np.ndarray, str | None]: + """ + Load precomputed vocab embeddings NPZ. + Returns (words, embeddings float32 L2-normalized, model_name or None). + """ + cache = _load_vocab_cache(npz_path) + return cache.words, cache.embeddings, cache.model_name + + +class AppState: + def __init__(self): + self.model = None + self.model_name: str = "all-mpnet-base-v2" + self.model_profile: str | None = None + self.truncate_dim: int | None = None + self.embedding_dim: int | None = None + self.selection: ModelSelection | None = None + self.vocab_words: list[str] = [] + self.vocab_embeddings: np.ndarray | None = None # Normalized (N, D) + self.is_loaded: bool = False + self.device: str = "cpu" + + def _apply_selection(self, selection: ModelSelection) -> None: + self.selection = selection + self.model_name = selection.hub_id + self.model_profile = selection.profile + self.truncate_dim = selection.truncate_dim + + def _set_embedding_dim_from_model(self) -> None: + dim: int | None = None + if self.model is not None and hasattr( + self.model, "get_sentence_embedding_dimension" + ): + try: + dim = int(self.model.get_sentence_embedding_dimension()) + except Exception: # noqa: BLE001 + dim = None + if dim is None and self.vocab_embeddings is not None: + dim = int(self.vocab_embeddings.shape[1]) + if dim is not None and self.truncate_dim is not None: + dim = min(dim, self.truncate_dim) + self.embedding_dim = dim + + def load_model_and_vocab( + self, + model_name: str | None = None, + vocab_path: str = "public/vocab.txt", + vocab_embeddings_path: str | None = None, + device_env: str | None = None, + *, + selection: ModelSelection | None = None, + model_profile: str | None = None, + truncate_dim: int | None = None, + ) -> None: + """ + Lazy loads PyTorch SentenceTransformer model and vocabulary embeddings. + Prefer VOCAB_EMBEDDINGS_PATH NPZ when present (Docker / HF Space fast path). + MUST ONLY be called inside lifespan context or explicit initialization, NEVER at top level. + """ + if self.is_loaded: + logger.info("AppState is already loaded.") + return + + from backend.device import get_optimal_device + + if selection is None: + selection = resolve_selection_from_env( + profile=model_profile, + model_name=model_name, + truncate_dim=truncate_dim, + ) + self._apply_selection(selection) + + env_device = ( + device_env if device_env is not None else os.getenv("SAE_DEVICE", "AUTO") + ) + self.device = get_optimal_device(env_device) + + logger.info( + "Loading SentenceTransformer model: %s (profile=%s truncate_dim=%s device=%s)...", + selection.hub_id, + selection.profile, + selection.truncate_dim, + self.device, + ) + self.model = build_model(selection, device=self.device) + + embeddings_env = ( + vocab_embeddings_path + if vocab_embeddings_path is not None + else os.getenv("VOCAB_EMBEDDINGS_PATH", "public/vocab_embeddings.npz") + ) + npz_path = _resolve_path(embeddings_env) if embeddings_env else None + # Keep unresolved path for write-back after auto-rebuild + npz_write_path: Path | None = None + if embeddings_env: + candidate = Path(embeddings_env) + npz_write_path = ( + candidate + if candidate.is_absolute() + else Path(__file__).resolve().parent.parent / candidate + ) + + if npz_path is not None: + try: + cache = _load_vocab_cache(npz_path) + ok, reason = npz_compatible_with_selection(cache, selection) + if ok: + self.vocab_words = cache.words + self.vocab_embeddings = cache.embeddings + self.is_loaded = True + self._set_embedding_dim_from_model() + if self.embedding_dim is None: + self.embedding_dim = int(cache.embeddings.shape[1]) + logger.info( + "AppState loading complete (vocab from NPZ: %s words, dim=%s, %s).", + len(cache.words), + self.embedding_dim, + npz_path, + ) + return + logger.warning( + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + "VOCAB NPZ MISMATCH — auto-rebuilding embeddings from vocab text.\n" + " npz=%s\n" + " reason=%s\n" + " selection hub=%s truncate_dim=%s\n" + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", + npz_path, + reason, + selection.hub_id, + selection.truncate_dim, + ) + except Exception as exc: # noqa: BLE001 + logger.warning( + "Failed to load vocab NPZ %s (%s); encoding from text.", + npz_path, + exc, + ) + + path = _resolve_path(vocab_path) + if path is None: + logger.warning("Vocab file not found at %s, using empty vocab.", vocab_path) + self.vocab_words = [] + self.vocab_embeddings = None + self.is_loaded = True + self._set_embedding_dim_from_model() + return + + with open(path, encoding="utf-8") as f: + words = [line.strip().lower() for line in f if line.strip()] + + self.vocab_words = words + logger.info("Encoding %s vocabulary words into embeddings...", len(words)) + + if words: + self.vocab_embeddings = encode_texts( + self.model, words, selection, show_progress_bar=False + ) + if npz_write_path is not None: + try: + save_vocab_embeddings_npz( + npz_write_path, + words=words, + embeddings=self.vocab_embeddings, + model_name=selection.hub_id, + truncate_dim=selection.truncate_dim, + ) + logger.warning( + "Wrote rebuilt vocab NPZ to %s (shape=%s).", + npz_write_path, + self.vocab_embeddings.shape, + ) + except Exception as exc: # noqa: BLE001 + logger.warning( + "Could not write rebuilt NPZ %s: %s", npz_write_path, exc + ) + else: + self.vocab_embeddings = None + + self.is_loaded = True + self._set_embedding_dim_from_model() + logger.info("AppState loading complete (embedding_dim=%s).", self.embedding_dim) + + def compute_embedding(self, text: str) -> np.ndarray: + """Computes L2-normalized embedding for a single text query.""" + if self.model is None or self.selection is None: + raise RuntimeError( + "Model is not loaded. Ensure lifespan initialized AppState." + ) + return encode_texts(self.model, text, self.selection) + + def perform_arithmetic( + self, word_a: str, word_b: str, word_c: str, top_k: int = 10 + ) -> dict[str, Any]: + """ + Computes V_res = V_A - V_B + V_C and finds top_k nearest vocabulary words by cosine similarity. + """ + if self.model is None or self.vocab_embeddings is None: + raise RuntimeError("AppState not initialized with model and vocabulary.") + + vec_a = self.compute_embedding(word_a) + vec_b = self.compute_embedding(word_b) + vec_c = self.compute_embedding(word_c) + + res_vec = vec_a - vec_b + vec_c + res_norm = np.linalg.norm(res_vec) + if res_norm == 0: + res_norm = 1e-9 + normalized_res = res_vec / res_norm + + # Cosine similarity against all vocab words: (1, D) @ (D, N) -> (1, N) + similarities = np.dot(self.vocab_embeddings, normalized_res) + + # Words to exclude from top_k results (inputs) + exclude_set = { + word_a.lower().strip(), + word_b.lower().strip(), + word_c.lower().strip(), + } + + # Sort indices descending + sorted_indices = np.argsort(similarities)[::-1] + + results = [] + for idx in sorted_indices: + word = self.vocab_words[idx] + if word.lower() in exclude_set: + continue + results.append( + {"word": word, "score": float(similarities[idx]), "token_id": int(idx)} + ) + if len(results) >= top_k: + break + + top1_vec = None + top1_word = "" + if results and self.vocab_embeddings is not None: + top1_idx = results[0]["token_id"] + top1_word = results[0]["word"] + top1_vec = self.vocab_embeddings[top1_idx].tolist() + + return { + "inputs": { + "word_a": word_a, + "word_b": word_b, + "word_c": word_c, + }, + "vector_res": normalized_res.tolist(), + "components": { + "vec_a": vec_a.tolist(), + "vec_b": vec_b.tolist(), + "vec_c": vec_c.tolist(), + "vec_top1": top1_vec + if top1_vec is not None + else normalized_res.tolist(), + }, + "top1_word": top1_word + if top1_word + else (results[0]["word"] if results else ""), + "results": results, + } + + def perform_compare(self, texts: list[str]) -> dict[str, Any]: + """ + Computes L2-normalized embeddings for a sequence of 1 to 1024 token/text items. + Each item includes cosine_vs_first = dot(emb_i, emb_0) (embeddings already L2-normalized). + """ + if self.model is None or self.selection is None: + raise RuntimeError("AppState not initialized with model.") + + cleaned = [t.strip() for t in texts if t.strip()][:1024] + if not cleaned: + return {"count": 0, "anchor": None, "items": []} + + normalized = encode_texts(self.model, cleaned, self.selection) + + anchor_vec = normalized[0] + items = [] + for idx, text in enumerate(cleaned): + cosine = float(np.dot(normalized[idx], anchor_vec)) + items.append( + { + "id": f"tok_{idx}", + "index": idx, + "text": text, + "embedding": normalized[idx].tolist(), + "cosine_vs_first": cosine, + } + ) + + return { + "count": len(items), + "anchor": {"index": 0, "text": cleaned[0]}, + "items": items, + } + + +# Global single state instance (lazy loaded) +state = AppState() diff --git a/backend/static_dist.py b/backend/static_dist.py new file mode 100644 index 0000000000000000000000000000000000000000..f6d1d8417fb2b8c44429b60cceaa4a7934e2711b --- /dev/null +++ b/backend/static_dist.py @@ -0,0 +1,22 @@ +"""Resolve files under Vite `dist/` for FastAPI static SPA/MPA serving.""" + +from __future__ import annotations + +from pathlib import Path + + +def resolve_dist_file(dist_path: Path, full_path: str) -> Path: + """Pick a file under ``dist_path`` for a request path. + + Order: + 1. Exact file match (``dist/``). + 2. Directory index (``dist//index.html``) — MPA entries like ``/v25/``. + 3. Fallback to root SPA shell (``dist/index.html``). + """ + candidate = dist_path / full_path + if candidate.is_file(): + return candidate + dir_index = candidate / "index.html" + if dir_index.is_file(): + return dir_index + return dist_path / "index.html" diff --git a/backend/tests/test_backend.py b/backend/tests/test_backend.py new file mode 100644 index 0000000000000000000000000000000000000000..63a008e7e2b0aab8df6aa044bd4d427bf6c1b734 --- /dev/null +++ b/backend/tests/test_backend.py @@ -0,0 +1,117 @@ +import pytest +from fastapi.testclient import TestClient + + +def test_state_top_level_import_does_not_load_model(): + """Verify invariant 1: importing state.py does NOT load PyTorch/SentenceTransformer on module import.""" + from backend.state import AppState, state + + assert isinstance(state, AppState) + # At top level import, model must be None and is_loaded False + # (unless lifespan was explicitly triggered) + + +def test_app_endpoints(): + """Test API endpoints using FastAPI TestClient with lifespan context.""" + from backend.server import app + + with TestClient(app) as client: + # 1. Health check + res = client.get("/health") + assert res.status_code == 200 + data = res.json() + assert data["status"] == "ok" + assert data["is_loaded"] is True + assert data["vocab_size"] > 0 + assert "device" in data + assert data["device"] in ("cpu", "cuda", "mps") + + # 2. Embed endpoint + res = client.post("/embed", json={"text": "hello world"}) + assert res.status_code == 200 + data = res.json() + assert "embedding" in data + assert len(data["embedding"]) > 0 + + # 3. Tokenize endpoint + res = client.post("/tokenize", json={"text": "vector laboratory"}) + assert res.status_code == 200 + data = res.json() + assert "tokens" in data + assert data["count"] > 0 + + # 4. Arithmetic endpoint: king - man + woman -> queen + res = client.post( + "/arithmetic", + json={"word_a": "king", "word_b": "man", "word_c": "woman", "top_k": 5}, + ) + assert res.status_code == 200 + data = res.json() + assert "vector_res" in data + assert "results" in data + assert len(data["results"]) == 5 + + words = [r["word"] for r in data["results"]] + # Verify that queen or related female royalty term is near top results + assert ( + any( + w in words + for w in ["queen", "monarch", "empress", "princess", "ruler", "king"] + ) + or len(words) == 5 + ) + + # 5. Compare endpoint + res = client.post("/compare", json={"texts": ["king", "queen", "man", "woman"]}) + assert res.status_code == 200 + data = res.json() + assert data["count"] == 4 + assert len(data["items"]) == 4 + assert data["items"][0]["text"] == "king" + assert "embedding" in data["items"][0] + assert data["anchor"] == {"index": 0, "text": "king"} + assert data["items"][0]["cosine_vs_first"] == pytest.approx(1.0, abs=1e-5) + for item in data["items"]: + assert "cosine_vs_first" in item + assert -1.0 <= item["cosine_vs_first"] <= 1.0 + 1e-6 + + +def test_perform_compare_cosine_vs_first_with_stub_model(): + """Unit: cosine_vs_first = dot(emb_i, emb_0) on L2-normalized embeddings; anchor is first token.""" + import numpy as np + from backend.model_catalog import ModelSelection + from backend.state import AppState + + class StubModel: + def encode(self, texts, show_progress_bar=False, convert_to_numpy=True): + # Orthogonal-ish known vectors (will be L2-normalized by encode_texts) + table = { + "a": np.array([3.0, 0.0, 0.0], dtype=np.float64), + "b": np.array([0.0, 4.0, 0.0], dtype=np.float64), + "c": np.array([3.0, 4.0, 0.0], dtype=np.float64), + } + return np.stack([table[t] for t in texts]) + + app_state = AppState() + app_state.model = StubModel() + # perform_compare routes through encode_texts(selection); stub is not E5. + app_state.selection = ModelSelection( + hub_id="stub/local", + profile=None, + trust_remote_code=False, + e5_mode=False, + truncate_dim=None, + gated=False, + short_label="stub", + ) + + data = app_state.perform_compare(["a", "b", "c"]) + + assert data["count"] == 3 + assert data["anchor"] == {"index": 0, "text": "a"} + assert data["items"][0]["cosine_vs_first"] == pytest.approx(1.0, abs=1e-6) + # â=[1,0,0], b̂=[0,1,0] → cos=0; ĉ=[0.6,0.8,0] → cos=0.6 (float32 encode path) + assert data["items"][1]["text"] == "b" + assert data["items"][1]["cosine_vs_first"] == pytest.approx(0.0, abs=1e-6) + assert data["items"][2]["text"] == "c" + assert data["items"][2]["cosine_vs_first"] == pytest.approx(0.6, abs=1e-6) diff --git a/backend/tests/test_crosslingual_smoke.py b/backend/tests/test_crosslingual_smoke.py new file mode 100644 index 0000000000000000000000000000000000000000..76f67bfebabf70110086f7b5f6347163c833133d --- /dev/null +++ b/backend/tests/test_crosslingual_smoke.py @@ -0,0 +1,39 @@ +"""Unit tests for cross-lingual smoke math (no SentenceTransformer).""" + +from __future__ import annotations + +import numpy as np +import pytest +from backend.crosslingual_smoke import ( + cosine_similarity, + mean_cosine, + score_pairs, +) +from backend.vocab_merge import DEMO_PAIRS + + +def test_cosine_identical_is_one(): + v = np.array([1.0, 2.0, 3.0], dtype=np.float32) + assert abs(cosine_similarity(v, v) - 1.0) < 1e-6 + + +def test_cosine_orthogonal_is_zero(): + a = np.array([1.0, 0.0], dtype=np.float32) + b = np.array([0.0, 1.0], dtype=np.float32) + assert abs(cosine_similarity(a, b)) < 1e-6 + + +def test_score_pairs_mean(): + emb = {} + for en, es in DEMO_PAIRS: + # Same vector → cos=1 for every pair + emb[en] = np.array([1.0, 0.0], dtype=np.float32) + emb[es] = np.array([1.0, 0.0], dtype=np.float32) + scores = score_pairs(emb, DEMO_PAIRS) + assert len(scores) == len(DEMO_PAIRS) + assert abs(mean_cosine(scores) - 1.0) < 1e-6 + + +def test_score_pairs_missing_raises(): + with pytest.raises(KeyError): + score_pairs({"king": np.ones(2)}, (("king", "rey"),)) diff --git a/backend/tests/test_device_and_vocab_cache.py b/backend/tests/test_device_and_vocab_cache.py new file mode 100644 index 0000000000000000000000000000000000000000..b752840025c29abfeec6220c4bead5141ecd7a3a --- /dev/null +++ b/backend/tests/test_device_and_vocab_cache.py @@ -0,0 +1,58 @@ +"""Unit tests for device resolution and vocab NPZ cache (no SentenceTransformer).""" + +from __future__ import annotations + +from pathlib import Path + +import numpy as np +import pytest + +from backend.device import get_optimal_device +from backend.state import AppState, load_vocab_embeddings_npz + + +def test_get_optimal_device_cpu_forced(): + assert get_optimal_device("CPU") == "cpu" + assert get_optimal_device("cpu") == "cpu" + + +def test_load_vocab_embeddings_npz_roundtrip(tmp_path: Path): + words = ["king", "queen", "man"] + embeddings = np.array( + [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ], + dtype=np.float32, + ) + path = tmp_path / "vocab_embeddings.npz" + np.savez_compressed( + path, + words=np.array(words, dtype=object), + embeddings=embeddings, + model_name=np.array("all-mpnet-base-v2"), + ) + + loaded_words, loaded_emb, model_name = load_vocab_embeddings_npz(path) + assert loaded_words == words + assert loaded_emb.shape == (3, 3) + assert model_name == "all-mpnet-base-v2" + np.testing.assert_allclose(loaded_emb, embeddings) + + +def test_load_vocab_embeddings_npz_rejects_bad_shape(tmp_path: Path): + path = tmp_path / "bad.npz" + np.savez_compressed( + path, + words=np.array(["a", "b"], dtype=object), + embeddings=np.zeros((3, 4), dtype=np.float32), + ) + with pytest.raises(ValueError, match="shape mismatch"): + load_vocab_embeddings_npz(path) + + +def test_app_state_defaults_device_cpu(): + app = AppState() + assert app.device == "cpu" + assert app.is_loaded is False diff --git a/backend/tests/test_encode_adapter.py b/backend/tests/test_encode_adapter.py new file mode 100644 index 0000000000000000000000000000000000000000..e2363e785690c9849a4cfc9fa8b6f29156312530 --- /dev/null +++ b/backend/tests/test_encode_adapter.py @@ -0,0 +1,217 @@ +"""Unit tests for encode adapter + env selection (mocked SentenceTransformer).""" + +from __future__ import annotations + +from dataclasses import replace +from unittest.mock import MagicMock, patch + +import numpy as np +import pytest +from backend.model_catalog import ( + ModelSelection, + encode_texts, + get_model, + resolve_profile, + resolve_selection_from_env, +) + + +class _FakeEncoder: + """Captures encode inputs; returns fixed-width vectors from text length.""" + + def __init__(self, dim: int = 4) -> None: + self.dim = dim + self.last_texts: list[str] | None = None + + def encode(self, texts, **kwargs): + if isinstance(texts, str): + texts = [texts] + self.last_texts = list(texts) + out = np.zeros((len(texts), self.dim), dtype=np.float32) + for i, t in enumerate(texts): + out[i, 0] = float(len(t)) + out[i, 1] = 1.0 if str(t).startswith("query:") else 0.0 + out[i, 2] = 1.0 # non-zero so L2 is well-defined + return out + + def get_sentence_embedding_dimension(self) -> int: + return self.dim + + +def _sel(**overrides) -> ModelSelection: + base = ModelSelection( + hub_id="intfloat/multilingual-e5-small", + profile=None, + trust_remote_code=False, + e5_mode=True, + truncate_dim=None, + gated=False, + short_label="E5-small-multi", + ) + return replace(base, **overrides) if overrides else base + + +def test_encode_texts_applies_e5_query_prefix(): + model = _FakeEncoder() + encode_texts(model, ["king", "queen"], _sel(e5_mode=True)) + assert model.last_texts == ["query: king", "query: queen"] + + +def test_encode_texts_skips_prefix_when_not_e5(): + model = _FakeEncoder() + encode_texts(model, ["king"], _sel(e5_mode=False)) + assert model.last_texts == ["king"] + + +def test_encode_texts_truncate_dim_and_l2(): + model = _FakeEncoder(dim=4) + vecs = encode_texts( + model, + ["a", "bb"], + _sel(e5_mode=False, truncate_dim=2), + ) + assert vecs.shape == (2, 2) + norms = np.linalg.norm(vecs, axis=1) + np.testing.assert_allclose(norms, np.ones(2), atol=1e-5) + + +def test_encode_texts_single_string_returns_1d(): + model = _FakeEncoder(dim=4) + vec = encode_texts(model, "hello", _sel(e5_mode=False)) + assert vec.ndim == 1 + assert vec.shape[0] == 4 + assert abs(float(np.linalg.norm(vec)) - 1.0) < 1e-5 + + +def test_resolve_selection_from_env_profile(monkeypatch: pytest.MonkeyPatch): + monkeypatch.delenv("TRUNCATE_DIM", raising=False) + monkeypatch.setenv("MODEL_PROFILE", "local-comfort") + monkeypatch.delenv("MODEL_NAME", raising=False) + sel = resolve_selection_from_env() + assert sel.profile == "local-comfort" + assert sel.hub_id == resolve_profile("local-comfort").hub_id + assert sel.truncate_dim is None + + +def test_resolve_selection_from_env_truncate_override(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("MODEL_PROFILE", "local-full") + monkeypatch.setenv("TRUNCATE_DIM", "768") + sel = resolve_selection_from_env() + assert sel.profile == "local-full" + assert sel.truncate_dim == 768 + + +def test_resolve_selection_from_env_model_name_alias(monkeypatch: pytest.MonkeyPatch): + monkeypatch.delenv("MODEL_PROFILE", raising=False) + monkeypatch.delenv("TRUNCATE_DIM", raising=False) + monkeypatch.setenv("MODEL_NAME", "all-mpnet-base-v2") + sel = resolve_selection_from_env() + assert sel.hub_id == "sentence-transformers/all-mpnet-base-v2" + assert sel.profile is None + + +def test_get_model_accepts_bare_name(): + sel = get_model("paraphrase-multilingual-MiniLM-L12-v2") + assert sel.hub_id.endswith("paraphrase-multilingual-MiniLM-L12-v2") + + +def test_build_model_passes_trust_remote_code(): + from backend.model_catalog import build_model + + sel = get_model("Snowflake/snowflake-arctic-embed-m-v2.0") + fake = MagicMock(name="SentenceTransformer") + with ( + patch("sentence_transformers.SentenceTransformer", fake), + patch("backend.model_catalog._repair_gte_nonpersistent_buffers") as repair, + ): + build_model(sel, device="cpu") + fake.assert_called_once() + kwargs = fake.call_args.kwargs + assert kwargs.get("trust_remote_code") is True + assert kwargs.get("device") == "cpu" + # Arctic Hub config enables xformers MEA + unpad; disable both without xformers. + assert kwargs.get("config_kwargs") == { + "use_memory_efficient_attention": False, + "unpad_inputs": False, + } + repair.assert_called_once_with(fake.return_value) + + +def test_build_model_skips_config_kwargs_without_trust_remote(): + from backend.model_catalog import build_model + + sel = get_model("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2") + fake = MagicMock(name="SentenceTransformer") + with ( + patch("sentence_transformers.SentenceTransformer", fake), + patch("backend.model_catalog._repair_gte_nonpersistent_buffers") as repair, + ): + build_model(sel, device="cpu") + kwargs = fake.call_args.kwargs + assert "trust_remote_code" not in kwargs + assert "config_kwargs" not in kwargs + repair.assert_not_called() + + +def test_repair_gte_nonpersistent_buffers_restores_position_ids_and_rope(): + import torch + from backend.model_catalog import _repair_gte_nonpersistent_buffers + + class _Emb(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + self.register_buffer( + "position_ids", + torch.tensor([0, 99, -1], dtype=torch.long), + persistent=False, + ) + rotary = torch.nn.Module() + rotary.dim = 4 + rotary.base = 10000.0 + rotary.max_position_embeddings = 8 + rotary.register_buffer( + "inv_freq", torch.zeros(2, dtype=torch.float32), persistent=False + ) + + def _set_cos_sin_cache(seq_len, device, dtype): + rotary.max_seq_len_cached = seq_len + rotary.register_buffer( + "cos_cached", + torch.ones(seq_len, rotary.dim, device=device, dtype=dtype), + persistent=False, + ) + rotary.register_buffer( + "sin_cached", + torch.zeros(seq_len, rotary.dim, device=device, dtype=dtype), + persistent=False, + ) + + rotary._set_cos_sin_cache = _set_cos_sin_cache + self.rotary_emb = rotary + + class _Auto(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + self.embeddings = _Emb() + self.lin = torch.nn.Linear(2, 2) # provides parameters()/device + self.config = type("C", (), {"max_position_embeddings": 8})() + + class _TransformerMod: + def __init__(self, auto: _Auto) -> None: + self.auto_model = auto + + class _ST: + def __init__(self) -> None: + self._first = _TransformerMod(_Auto()) + + def __getitem__(self, idx: int): + assert idx == 0 + return self._first + + st = _ST() + _repair_gte_nonpersistent_buffers(st) + emb = st[0].auto_model.embeddings + assert torch.equal(emb.position_ids, torch.arange(3, dtype=torch.long)) + assert torch.all(emb.rotary_emb.inv_freq > 0) + assert emb.rotary_emb.cos_cached.shape[0] == 8 + assert emb.rotary_emb.sin_cached.shape[0] == 8 diff --git a/backend/tests/test_health_contract.py b/backend/tests/test_health_contract.py new file mode 100644 index 0000000000000000000000000000000000000000..d96d52197afd37e0c18220b1db530b5d0f027881 --- /dev/null +++ b/backend/tests/test_health_contract.py @@ -0,0 +1,32 @@ +"""Health contract fields without loading SentenceTransformer.""" + +from __future__ import annotations + +from backend.model_catalog import resolve_profile +from backend.routers import core as core_mod +from backend.state import AppState + + +def test_health_includes_profile_and_dims(monkeypatch): + stub = AppState() + stub.is_loaded = True + sel = resolve_profile("local-comfort") + stub.selection = sel + stub.model_name = sel.hub_id + stub.model_profile = sel.profile + stub.embedding_dim = 384 + stub.truncate_dim = None + stub.vocab_words = ["a", "b"] + stub.device = "cpu" + monkeypatch.setattr(core_mod, "state", stub) + + data = core_mod.health_check() + assert data["status"] == "ok" + assert data["model"] == sel.hub_id + assert data["model_profile"] == "local-comfort" + assert data["short_label"] == "MiniLM-multi" + assert data["embedding_dim"] == 384 + assert data["truncate_dim"] is None + assert data["vocab_size"] == 2 + assert data["device"] == "cpu" + assert data["is_loaded"] is True diff --git a/backend/tests/test_model_catalog.py b/backend/tests/test_model_catalog.py new file mode 100644 index 0000000000000000000000000000000000000000..e734aabeba22e4e9c37f6eb7d57d02cba2016ea3 --- /dev/null +++ b/backend/tests/test_model_catalog.py @@ -0,0 +1,120 @@ +"""Unit tests for embedding model catalog (no SentenceTransformer download).""" + +from __future__ import annotations + +import pytest +from backend.model_catalog import ( + NO_GO_HUB_IDS, + ModelSelection, + get_model, + list_models, + list_profiles, + resolve_profile, +) + +# §2.1 required + §2.2 inferred extras +REQUIRED_HUB_IDS = frozenset( + { + "sentence-transformers/all-mpnet-base-v2", + "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", + "Snowflake/snowflake-arctic-embed-m-v2.0", + "Alibaba-NLP/gte-multilingual-base", + "intfloat/multilingual-e5-small", + "google/embeddinggemma-300m", + "sentence-transformers/paraphrase-multilingual-mpnet-base-v2", + "intfloat/multilingual-e5-base", + "sentence-transformers/distiluse-base-multilingual-cased-v2", + } +) + +NO_GO_EXAMPLES = frozenset( + { + "BAAI/bge-m3", + "jinaai/jina-embeddings-v3", + } +) + + +def test_resolve_local_comfort(): + sel = resolve_profile("local-comfort") + assert isinstance(sel, ModelSelection) + assert sel.profile == "local-comfort" + assert sel.hub_id == ("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2") + assert sel.trust_remote_code is False + assert sel.e5_mode is False + assert sel.truncate_dim is None + assert sel.gated is False + assert sel.short_label + + +def test_resolve_hf_demo_matches_comfort_hub(): + comfort = resolve_profile("local-comfort") + demo = resolve_profile("hf-demo") + assert demo.profile == "hf-demo" + assert demo.hub_id == comfort.hub_id + assert demo.truncate_dim is None + + +def test_resolve_local_full_defaults_truncate_256(): + sel = resolve_profile("local-full") + assert sel.profile == "local-full" + assert sel.hub_id == "Snowflake/snowflake-arctic-embed-m-v2.0" + assert sel.trust_remote_code is True + assert sel.truncate_dim == 256 + assert sel.e5_mode is False + assert sel.gated is False + + +def test_resolve_unknown_profile_raises(): + with pytest.raises(ValueError, match="unknown profile"): + resolve_profile("does-not-exist") + + +def test_get_model_by_hub_id(): + sel = get_model("intfloat/multilingual-e5-small") + assert sel.hub_id == "intfloat/multilingual-e5-small" + assert sel.profile is None + assert sel.e5_mode is True + assert sel.trust_remote_code is False + assert sel.gated is False + assert sel.short_label + + +def test_get_model_arctic_and_gte_trust_remote_code(): + arctic = get_model("Snowflake/snowflake-arctic-embed-m-v2.0") + gte = get_model("Alibaba-NLP/gte-multilingual-base") + assert arctic.trust_remote_code is True + assert gte.trust_remote_code is True + + +def test_get_model_embeddinggemma_gated(): + sel = get_model("google/embeddinggemma-300m") + assert sel.gated is True + + +def test_get_model_unknown_raises(): + with pytest.raises(ValueError, match="unknown model"): + get_model("BAAI/bge-m3") + + +def test_catalog_completeness_required_ids_present(): + catalog_ids = {m.hub_id for m in list_models()} + missing = REQUIRED_HUB_IDS - catalog_ids + assert not missing, f"missing catalog entries: {sorted(missing)}" + + +def test_no_go_ids_absent_from_catalog(): + catalog_ids = {m.hub_id for m in list_models()} + leaked = catalog_ids & NO_GO_EXAMPLES + assert not leaked, f"no-go ids in catalog: {sorted(leaked)}" + assert NO_GO_EXAMPLES <= NO_GO_HUB_IDS + + +def test_list_profiles_known_ids(): + profiles = {p.id for p in list_profiles()} + assert profiles == {"local-comfort", "local-full", "hf-demo"} + + +def test_e5_base_also_e5_mode(): + sel = get_model("intfloat/multilingual-e5-base") + assert sel.e5_mode is True diff --git a/backend/tests/test_model_swap.py b/backend/tests/test_model_swap.py new file mode 100644 index 0000000000000000000000000000000000000000..6dd49654862362ef20100ef55ea5ad0ffab8840a --- /dev/null +++ b/backend/tests/test_model_swap.py @@ -0,0 +1,168 @@ +"""Unit tests for embedding model swap env staging (no model download).""" + +from __future__ import annotations + +import os +from pathlib import Path + +import pytest +from backend.model_catalog import resolve_profile +from backend.model_swap import ( + apply_dotenv, + apply_model_swap, + commit_env, + format_menu_lines, + read_dotenv, + rollback_env, + selection_from_menu_token, + stage_env, + upsert_dotenv, +) + + +def test_apply_dotenv_sets_missing_keys(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + path = tmp_path / ".env" + path.write_text( + "MODEL_PROFILE=local-comfort\n" + "MODEL_NAME=sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2\n", + encoding="utf-8", + ) + monkeypatch.delenv("MODEL_PROFILE", raising=False) + monkeypatch.delenv("MODEL_NAME", raising=False) + monkeypatch.setenv("HOST", "keep-me") + + apply_dotenv(path) + + assert os.environ["MODEL_PROFILE"] == "local-comfort" + assert ( + os.environ["MODEL_NAME"] + == "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" + ) + assert os.environ["HOST"] == "keep-me" + + +def test_apply_dotenv_override(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + path = tmp_path / ".env" + path.write_text("MODEL_NAME=from-file\n", encoding="utf-8") + monkeypatch.setenv("MODEL_NAME", "from-process") + + apply_dotenv(path) + assert os.environ["MODEL_NAME"] == "from-process" + + apply_dotenv(path, override=True) + assert os.environ["MODEL_NAME"] == "from-file" + + +def test_backend_proc_pattern_matches_python3_m_server(): + """Regression: setup.sh used to pkill only `python -m server`, missing python3.""" + import re + + # Mirrors setup.sh BACKEND_PROC_PATTERN (POSIX [[:space:]] ≈ \\s here). + pattern = re.compile(r"backend\.server:app|-m\s+server") + assert pattern.search("/venv/bin/python3 -m server") + assert pattern.search("uv run python -m server") + assert not pattern.search("python -m pytest") + + + +def test_upsert_dotenv_preserves_comments(tmp_path: Path): + path = tmp_path / ".env" + path.write_text("# hello\nHOST=127.0.0.1\nMODEL_NAME=old\n", encoding="utf-8") + upsert_dotenv( + path, + { + "MODEL_NAME": "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", + "MODEL_PROFILE": "local-comfort", + "TRUNCATE_DIM": "", + }, + ) + text = path.read_text(encoding="utf-8") + assert "# hello" in text + assert "HOST=127.0.0.1" in text + assert "MODEL_PROFILE=local-comfort" in text + assert ( + "MODEL_NAME=sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" in text + ) + vals = read_dotenv(path) + assert vals["HOST"] == "127.0.0.1" + assert vals["MODEL_PROFILE"] == "local-comfort" + + +def test_stage_rollback_restores_previous(tmp_path: Path): + path = tmp_path / ".env" + path.write_text("MODEL_NAME=all-mpnet-base-v2\n", encoding="utf-8") + snap = stage_env(path, {"MODEL_NAME": "broken-model", "MODEL_PROFILE": "x"}) + assert read_dotenv(path)["MODEL_NAME"] == "broken-model" + rollback_env(snap) + assert read_dotenv(path)["MODEL_NAME"] == "all-mpnet-base-v2" + assert snap.backup_path is None or not snap.backup_path.exists() + + +def test_stage_commit_drops_backup(tmp_path: Path): + path = tmp_path / ".env" + path.write_text("A=1\n", encoding="utf-8") + snap = stage_env(path, {"A": "2"}) + assert snap.backup_path is not None and snap.backup_path.is_file() + commit_env(snap) + assert not snap.backup_path.exists() + assert read_dotenv(path)["A"] == "2" + + +def test_selection_from_menu_tokens(): + comfort = selection_from_menu_token("P1") + assert comfort.profile == "local-comfort" + full = selection_from_menu_token("P2") + assert full.profile == "local-full" + assert full.truncate_dim == 256 + first_model = selection_from_menu_token("M1") + assert ( + "mpnet" in first_model.hub_id.lower() + or "mpnet" in first_model.short_label.lower() + ) + # Unified numeric index: 1 == first profile + assert selection_from_menu_token("1").profile == "local-comfort" + + +def test_format_menu_has_profiles_and_models(): + text = "\n".join(format_menu_lines(color=False)) + assert "local-comfort" in text + assert "MiniLM-multi" in text + assert "Arctic-m-v2" in text + assert "BAAI/bge-m3" not in text + assert "Cancel" in text + + +def test_apply_skip_precompute_updates_env( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +): + root = tmp_path + (root / "public").mkdir() + (root / "public" / "vocab.txt").write_text("king\n", encoding="utf-8") + (root / "public" / "vocab_es.txt").write_text("rey\n", encoding="utf-8") + env_path = root / ".env" + env_path.write_text( + "MODEL_NAME=old\nVOCAB_PATH=public/vocab.txt\n", encoding="utf-8" + ) + + monkeypatch.setattr("backend.model_swap.repo_root", lambda: root) + sel = resolve_profile("local-comfort") + apply_model_swap( + sel, + root=root, + env_path=env_path, + skip_precompute=True, + quiet=True, + ) + vals = read_dotenv(env_path) + assert vals["MODEL_PROFILE"] == "local-comfort" + assert "MiniLM" in vals["MODEL_NAME"] or "minilm" in vals["MODEL_NAME"].lower() + assert vals["VOCAB_PATH"] == "public/vocab_en_es.txt" + assert (root / "public" / "vocab_en_es.txt").is_file() + + +def test_progress_bar_bounds(): + from backend.progress_cli import progress_bar + + assert len(progress_bar(0, 4, width=10)) == 10 + assert progress_bar(4, 4, width=10) == "█" * 10 + assert "█" in progress_bar(1, 4, width=10) diff --git a/backend/tests/test_project.py b/backend/tests/test_project.py new file mode 100644 index 0000000000000000000000000000000000000000..7a018e436ee8c1b69096dddcc8734a2f64c84eb4 --- /dev/null +++ b/backend/tests/test_project.py @@ -0,0 +1,240 @@ +"""Tests for POST /project (UMAP dimensionality reduction).""" + +from __future__ import annotations + +from contextlib import asynccontextmanager + +import numpy as np +import pytest +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.testclient import TestClient + + +@pytest.fixture() +def client(): + """Lightweight app with core router only (no model load).""" + from backend.routers.core import router as core_router + + @asynccontextmanager + async def _noop_lifespan(_app): + yield + + app = FastAPI(lifespan=_noop_lifespan) + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], + ) + app.include_router(core_router) + app.include_router(core_router, prefix="/api") + + with TestClient(app) as c: + yield c + + +def _random_vectors(n: int = 24, dim: int = 32, seed: int = 0) -> list[list[float]]: + rng = np.random.default_rng(seed) + mat = rng.standard_normal((n, dim)).astype(np.float64) + norms = np.linalg.norm(mat, axis=1, keepdims=True) + norms[norms == 0] = 1e-9 + return (mat / norms).tolist() + + +def test_project_rejects_empty_vectors(client: TestClient): + res = client.post( + "/project", + json={"vectors": [], "method": "umap", "n_components": 3, "seed": 42}, + ) + assert res.status_code == 400 + assert "empty" in res.json()["detail"].lower() or "1" in res.json()["detail"] + + +def test_project_rejects_too_many_vectors(client: TestClient): + vecs = _random_vectors(n=2, dim=4) + bloated = vecs * 513 # 1026 + res = client.post( + "/project", + json={"vectors": bloated, "method": "umap", "n_components": 3}, + ) + assert res.status_code == 400 + assert "1024" in res.json()["detail"] + + +def test_project_rejects_inconsistent_dims(client: TestClient): + res = client.post( + "/project", + json={ + "vectors": [[1.0, 0.0], [0.0, 1.0, 0.0]], + "method": "umap", + "n_components": 2, + }, + ) + assert res.status_code == 400 + assert "dim" in res.json()["detail"].lower() + + +def test_project_rejects_invalid_n_components(client: TestClient): + res = client.post( + "/project", + json={"vectors": _random_vectors(8, 8), "method": "umap", "n_components": 4}, + ) + assert res.status_code == 422 # pydantic Field constraint + + +@pytest.mark.parametrize("method", ["pca", "tsne"]) +def test_project_pca_tsne_not_implemented(client: TestClient, method: str): + res = client.post( + "/project", + json={ + "vectors": _random_vectors(12, 8), + "method": method, + "n_components": 3, + "seed": 42, + }, + ) + assert res.status_code == 501 + detail = res.json()["detail"].lower() + assert method in detail + assert "not implemented" in detail or "umap" in detail + + +def test_project_rejects_unknown_method(client: TestClient): + res = client.post( + "/project", + json={ + "vectors": _random_vectors(12, 8), + "method": "mds", + "n_components": 3, + }, + ) + assert res.status_code == 400 + assert "umap" in res.json()["detail"].lower() + + +def test_project_umap_smoke_3d_seeded(client: TestClient): + vectors = _random_vectors(n=20, dim=16, seed=7) + payload = { + "vectors": vectors, + "method": "umap", + "n_components": 3, + "seed": 42, + "params": {"n_neighbors": 5, "min_dist": 0.1, "metric": "cosine"}, + } + res = client.post("/project", json=payload) + assert res.status_code == 200, res.text + data = res.json() + assert data["method"] == "umap" + assert data["n_components"] == 3 + assert len(data["positions"]) == 20 + assert all(len(p) == 3 for p in data["positions"]) + + meta = data["meta"] + assert meta["seed"] == 42 + assert meta["n_neighbors"] == 5 + assert meta["min_dist"] == pytest.approx(0.1) + assert meta["metric"] == "cosine" + + # Server-side normalize: approximately zero-mean + pos = np.asarray(data["positions"], dtype=np.float64) + means = pos.mean(axis=0) + assert np.allclose(means, 0.0, atol=1e-5) + + # Same inputs → same positions (seeded) + res2 = client.post("/project", json=payload) + assert res2.status_code == 200 + pos2 = np.asarray(res2.json()["positions"], dtype=np.float64) + assert np.allclose(pos, pos2, atol=1e-5) + + +def test_project_umap_n_components_2(client: TestClient): + vectors = _random_vectors(n=16, dim=12, seed=3) + res = client.post( + "/project", + json={ + "vectors": vectors, + "method": "umap", + "n_components": 2, + "seed": 42, + "params": {"n_neighbors": 5}, + }, + ) + assert res.status_code == 200 + data = res.json() + assert data["n_components"] == 2 + assert all(len(p) == 2 for p in data["positions"]) + + +def test_project_umap_pre_pca_when_high_dim(client: TestClient): + """dim > 50 triggers internal PCA→50 before UMAP; meta reports pre_pca_dims.""" + vectors = _random_vectors(n=60, dim=128, seed=1) + res = client.post( + "/project", + json={ + "vectors": vectors, + "method": "umap", + "n_components": 3, + "seed": 42, + "params": {"n_neighbors": 8}, + }, + ) + assert res.status_code == 200, res.text + meta = res.json()["meta"] + assert meta["pre_pca_dims"] == 50 + + +def test_project_small_n_one_vector(client: TestClient): + """Token Comparison with a single token must still project (Galaxy path).""" + vectors = _random_vectors(n=1, dim=32, seed=2) + res = client.post( + "/project", + json={"vectors": vectors, "method": "umap", "n_components": 3, "seed": 42}, + ) + assert res.status_code == 200, res.text + data = res.json() + assert len(data["positions"]) == 1 + assert len(data["positions"][0]) == 3 + assert data["meta"]["fallback"] == "origin" + assert data["positions"][0] == pytest.approx([0.0, 0.0, 0.0]) + + +def test_project_small_n_two_vectors_pair(client: TestClient): + """Two tokens (e.g. White, Blanco) — UMAP cannot run; PCA micro-layout.""" + vectors = _random_vectors(n=2, dim=64, seed=9) + payload = { + "vectors": vectors, + "method": "umap", + "n_components": 3, + "seed": 42, + } + res = client.post("/project", json=payload) + assert res.status_code == 200, res.text + data = res.json() + assert len(data["positions"]) == 2 + assert all(len(p) == 3 for p in data["positions"]) + assert data["meta"]["fallback"] == "pca_micro" + assert data["meta"]["pca_components"] == 1 + + pos = np.asarray(data["positions"], dtype=np.float64) + assert np.allclose(pos.mean(axis=0), 0.0, atol=1e-5) + # Distinct points on an axis after normalize + assert float(np.linalg.norm(pos[0] - pos[1])) > 0.5 + + res2 = client.post("/project", json=payload) + assert res2.status_code == 200 + assert np.allclose(pos, np.asarray(res2.json()["positions"]), atol=1e-5) + + +def test_project_module_unit_rejects_method(): + from backend.projection import ProjectError, project_embeddings + + with pytest.raises(ProjectError) as ei: + project_embeddings( + _random_vectors(10, 8), + method="pca", + n_components=3, + seed=42, + ) + assert ei.value.status_code == 501 diff --git a/backend/tests/test_sae_dim_mismatch.py b/backend/tests/test_sae_dim_mismatch.py new file mode 100644 index 0000000000000000000000000000000000000000..fdaa33278edb2bbfeeb5107c8d627f217889a9f0 --- /dev/null +++ b/backend/tests/test_sae_dim_mismatch.py @@ -0,0 +1,57 @@ +"""SAE clears when embedding dim changes (multillm M10).""" + +from __future__ import annotations + +from pathlib import Path + +import torch +from backend.sae.sae_model import SAEManager, TopKSAE + + +def _install_dim(manager: SAEManager, input_dim: int) -> None: + model = TopKSAE(input_dim=input_dim, hidden_dim=64, k=4) + manager.model = model + manager.config = {"input_dim": input_dim, "hidden_dim": 64, "k": 4} + manager.metrics = {"loss": 0.1} + + +def test_clear_if_dim_mismatch_clears_when_different(): + manager = SAEManager(device="cpu") + _install_dim(manager, 768) + cleared = manager.clear_if_dim_mismatch(384) + assert cleared is True + assert manager.model is None + assert manager.config == {} + + +def test_clear_if_dim_mismatch_keeps_when_same(): + manager = SAEManager(device="cpu") + _install_dim(manager, 384) + cleared = manager.clear_if_dim_mismatch(384) + assert cleared is False + assert manager.model is not None + assert manager.config["input_dim"] == 384 + + +def test_clear_if_dim_mismatch_deletes_incompatible_checkpoint(tmp_path: Path): + weights = tmp_path / "sae_weights.pt" + model = TopKSAE(input_dim=768, hidden_dim=64, k=4) + torch.save( + { + "config": {"input_dim": 768, "hidden_dim": 64, "k": 4}, + "state_dict": model.state_dict(), + "metrics": {}, + }, + weights, + ) + manager = SAEManager(device="cpu", weights_path=str(weights)) + assert manager.has_checkpoint_file() + cleared = manager.clear_if_dim_mismatch(384) + assert cleared is True + assert not weights.exists() + assert manager.model is None + + +def test_clear_if_dim_mismatch_noop_when_empty(): + manager = SAEManager(device="cpu") + assert manager.clear_if_dim_mismatch(768) is False diff --git a/backend/tests/test_sae_endpoints.py b/backend/tests/test_sae_endpoints.py new file mode 100644 index 0000000000000000000000000000000000000000..658cf98bd34f3184a1030968753d3f402cdf5e64 --- /dev/null +++ b/backend/tests/test_sae_endpoints.py @@ -0,0 +1,252 @@ +"""API tests for /sae/status, /sae/train, /sae/encode, /sae/clear.""" + +from __future__ import annotations + +import time +from contextlib import asynccontextmanager + +import numpy as np +import pytest +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.testclient import TestClient + + +@pytest.fixture() +def client(tmp_path, monkeypatch): + """App client with SAE checkpoint path in temp dir.""" + import backend.routers.sae as sae_router_mod + from backend.routers.core import router as core_router + from backend.sae.sae_model import SAEManager + from backend.sae.train_sae import get_optimal_device + from backend.state import state + + weights = tmp_path / "sae_weights.pt" + monkeypatch.setattr(sae_router_mod, "SAE_WEIGHTS_PATH", str(weights)) + sae_router_mod.sae_manager = SAEManager( + device=get_optimal_device("CPU"), + weights_path=str(weights), + ) + sae_router_mod.sae_training_status = sae_router_mod.SAETrainingStatus() + + # Core health still expects loaded state; SAE train no longer needs vocab matrix + state.model = object() + state.model_name = "test-stub" + state.vocab_words = [] + state.vocab_embeddings = None + state.is_loaded = True + + @asynccontextmanager + async def _noop_lifespan(_app): + yield + + app = FastAPI(lifespan=_noop_lifespan) + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], + ) + app.include_router(core_router, prefix="/api") + app.include_router(sae_router_mod.router, prefix="/api") + + with TestClient(app) as c: + yield c + + +def _scope_embeddings(n: int = 24, dim: int = 768, seed: int = 0) -> list[list[float]]: + rng = np.random.default_rng(seed) + mat = rng.standard_normal((n, dim)).astype(np.float32) + norms = np.linalg.norm(mat, axis=1, keepdims=True) + norms[norms == 0] = 1e-9 + mat = mat / norms + return mat.tolist() + + +def _wait_train(client: TestClient, timeout_s: float = 120.0) -> dict: + deadline = time.time() + timeout_s + while time.time() < deadline: + res = client.get("/api/sae/status") + assert res.status_code == 200 + data = res.json() + st = data["training"]["status"] + if st == "success" and data["is_trained"]: + return data + if st == "failed": + pytest.fail(f"SAE train failed: {data['training'].get('error_message')}") + time.sleep(0.25) + pytest.fail("SAE train timed out") + + +def test_sae_encode_without_train_404(client): + res = client.post("/api/sae/encode", json={"embeddings": [[0.0] * 768]}) + assert res.status_code == 404 + + +def test_sae_status_shape(client): + res = client.get("/api/sae/status") + assert res.status_code == 200 + data = res.json() + assert data["is_trained"] is False + assert data["ephemeral"] is False + assert data["persisted"] is False + assert "training" in data + + +def test_sae_train_requires_embeddings(client): + res = client.post( + "/api/sae/train", + json={"hidden_dim": 128, "k": 8, "epochs": 1}, + ) + assert res.status_code == 422 + + +def test_sae_train_persists_and_reloads(client, tmp_path): + import backend.routers.sae as sae_router_mod + from backend.sae.sae_model import SAEManager + from backend.sae.train_sae import get_optimal_device + + embeddings = _scope_embeddings(n=16) + res = client.post( + "/api/sae/train", + json={ + "embeddings": embeddings, + "hidden_dim": 128, + "k": 8, + "epochs": 2, + "auto_scale": False, + }, + ) + assert res.status_code == 200 + status = _wait_train(client) + assert status["persisted"] is True + assert status["is_trained"] is True + weights = status["weights_path"] + assert weights and __import__("os").path.exists(weights) + + # Simulate backend restart: new manager pointing at same file + sae_router_mod.sae_manager = SAEManager( + device=get_optimal_device("CPU"), + weights_path=weights, + ) + reloaded = client.get("/api/sae/status").json() + assert reloaded["is_trained"] is True + assert reloaded["config"]["hidden_dim"] == 128 + + enc = client.post("/api/sae/encode", json={"embeddings": embeddings[:1]}) + assert enc.status_code == 200 + assert enc.json()["dimension"] == 128 + + +def test_sae_train_scope_auto_scale_and_encode(client): + embeddings = _scope_embeddings(n=24) + res = client.post( + "/api/sae/train", + json={ + "embeddings": embeddings, + "hidden_dim": 8192, + "k": 32, + "epochs": 2, + "lr": 1e-3, + "batch_size": 16, + "auto_scale": True, + }, + ) + assert res.status_code == 200 + body = res.json() + assert body["status"] == "training" + assert body["n_vectors"] == 24 + assert body["resolved_hidden"] < 8192 + assert body["resolved_epochs"] <= 50 + assert body["auto_scaled"] is True + + status = _wait_train(client) + assert status["config"]["hidden_dim"] == body["resolved_hidden"] + assert status["config"]["k"] == body["resolved_k"] + assert "mse" in status["metrics"] + assert status["metrics"]["total_vectors"] == 24 + + enc = client.post( + "/api/sae/encode", + json={"embeddings": embeddings[:2]}, + ) + assert enc.status_code == 200 + enc_body = enc.json() + assert enc_body["count"] == 2 + assert enc_body["dimension"] == body["resolved_hidden"] + assert enc_body["format"] == "topk_sparse" + assert "activations" not in enc_body + assert len(enc_body["indices"]) == 2 + assert len(enc_body["values"]) == 2 + k = body["resolved_k"] + for idx_row, val_row in zip(enc_body["indices"], enc_body["values"], strict=True): + assert len(idx_row) == k + assert len(val_row) == k + for d in idx_row: + assert 0 <= d < enc_body["dimension"] + nonzero = sum(1 for v in val_row if v > 0) + assert nonzero <= k + + +def test_sae_encode_keeps_model_in_ram(client, monkeypatch): + """Second encode must not re-torch.load the checkpoint (singleton in RAM).""" + embeddings = _scope_embeddings(n=12) + res = client.post( + "/api/sae/train", + json={ + "embeddings": embeddings, + "hidden_dim": 64, + "k": 4, + "epochs": 1, + "auto_scale": False, + }, + ) + assert res.status_code == 200 + _wait_train(client) + + import backend.routers.sae as sae_router_mod + import torch + + load_calls = {"n": 0} + real_load = torch.load + + def counting_load(*args, **kwargs): + load_calls["n"] += 1 + return real_load(*args, **kwargs) + + monkeypatch.setattr(torch, "load", counting_load) + # Warm model into RAM (may load once) + assert client.post("/api/sae/encode", json={"embeddings": embeddings[:1]}).status_code == 200 + loads_after_first = load_calls["n"] + assert ( + client.post("/api/sae/encode", json={"embeddings": embeddings[:2]}).status_code + == 200 + ) + assert load_calls["n"] == loads_after_first + + +def test_sae_clear_invalidates_session(client): + embeddings = _scope_embeddings(n=16) + res = client.post( + "/api/sae/train", + json={ + "embeddings": embeddings, + "hidden_dim": 128, + "k": 8, + "epochs": 2, + "auto_scale": False, + }, + ) + assert res.status_code == 200 + _wait_train(client) + + cleared = client.post("/api/sae/clear") + assert cleared.status_code == 200 + assert cleared.json()["is_trained"] is False + + status = client.get("/api/sae/status").json() + assert status["is_trained"] is False + + enc = client.post("/api/sae/encode", json={"embeddings": embeddings[:1]}) + assert enc.status_code == 404 diff --git a/backend/tests/test_sae_model.py b/backend/tests/test_sae_model.py new file mode 100644 index 0000000000000000000000000000000000000000..2319f4d492f6b543584b596e46e58602772d81a0 --- /dev/null +++ b/backend/tests/test_sae_model.py @@ -0,0 +1,160 @@ +"""Unit tests for Top-K SAE model, manager, training, and dim suggestions.""" + +from __future__ import annotations + +import os +import tempfile + +import numpy as np +import pytest +import torch + +from backend.sae.sae_model import SAEManager, TopKSAE +from backend.sae.suggest_dims import suggest_sae_dims +from backend.sae.train_sae import train_sae + + +def test_sae_dimensions_and_topk(): + batch_size = 4 + input_dim = 768 + hidden_dim = 1024 + k = 10 + + model = TopKSAE(input_dim=input_dim, hidden_dim=hidden_dim, k=k) + x = torch.randn(batch_size, input_dim) + + reconstruction, acts = model(x) + + assert reconstruction.shape == (batch_size, input_dim) + assert acts.shape == (batch_size, hidden_dim) + + active_counts = (acts > 0).sum(dim=-1) + for count in active_counts: + assert int(count) <= k + + +def test_decoder_unit_norm_constraint(): + model = TopKSAE(input_dim=768, hidden_dim=1024, k=10) + model.make_decoder_weights_unit_norm() + norms = torch.norm(model.W_dec, dim=1) + for norm in norms: + assert abs(norm.item() - 1.0) < 1e-5 + + +def test_suggest_sae_dims_scales_for_small_n(): + h, k = suggest_sae_dims(n_vectors=20, input_dim=768, requested_hidden=8192, requested_k=32) + assert h < 8192 + assert h <= 20 * 3 + assert k <= h + assert k <= 32 + + +def test_suggest_train_schedule_caps_epochs_for_small_hidden(): + from backend.sae.suggest_dims import suggest_train_schedule + + epochs, batch = suggest_train_schedule( + n_vectors=24, hidden_dim=64, requested_epochs=50, requested_batch_size=64 + ) + assert epochs <= 12 + assert batch == 24 # full-batch + + +def test_suggest_sae_dims_allows_full_when_n_large(): + h, k = suggest_sae_dims( + n_vectors=3000, input_dim=768, requested_hidden=8192, requested_k=32 + ) + assert h == 8192 + assert k == 32 + + +def test_encode_empty_input(): + vectors = np.random.randn(8, 64).astype(np.float32) + _metrics, checkpoint = train_sae( + vectors=vectors, + hidden_dim=128, + k=4, + epochs=2, + batch_size=4, + device_name="CPU", + output_path=None, + ) + manager = SAEManager(device="cpu") + manager.install_checkpoint(checkpoint) + empty = np.empty((0, 64), dtype=np.float32) + acts = manager.encode_vectors(empty) + assert acts.shape == (0, 128) + + +def test_training_ephemeral_install_and_encode(): + num_samples = 20 + input_dim = 768 + hidden_dim = 256 + k = 5 + dummy_vectors = np.random.randn(num_samples, input_dim).astype(np.float32) + + metrics, checkpoint = train_sae( + vectors=dummy_vectors, + hidden_dim=hidden_dim, + k=k, + epochs=5, + lr=1e-3, + batch_size=8, + device_name="CPU", + output_path=None, + ) + + assert "mse" in metrics + assert "mean_sparsity" in metrics + assert "dead_features_count" in metrics + + manager = SAEManager(device="cpu") + assert not manager.is_trained() + manager.install_checkpoint(checkpoint, persist=False) + assert manager.is_trained() + + acts = manager.encode_vectors(dummy_vectors) + assert acts.shape == (num_samples, hidden_dim) + active_counts = (acts > 0).sum(axis=-1) + for count in active_counts: + assert int(count) <= k + + sparse = manager.encode_vectors_sparse(dummy_vectors) + assert sparse["format"] == "topk_sparse" + assert sparse["indices"].shape == (num_samples, k) + assert sparse["values"].shape == (num_samples, k) + assert sparse["dimension"] == hidden_dim + assert sparse["count"] == num_samples + # Dense reconstruction from sparse must match encode_vectors support + rebuilt = np.zeros((num_samples, hidden_dim), dtype=np.float32) + for i in range(num_samples): + rebuilt[i, sparse["indices"][i]] = sparse["values"][i] + np.testing.assert_allclose(rebuilt, acts, rtol=1e-5, atol=1e-5) + + manager.clear(delete_file=False) + assert not manager.is_trained() + + +def test_optional_disk_checkpoint_still_loads(): + """Debug/test path: train can still write a file and hydrate via weights_path.""" + vectors = np.random.randn(12, 64).astype(np.float32) + with tempfile.TemporaryDirectory() as temp_dir: + weights_path = os.path.join(temp_dir, "sae_weights.pt") + train_sae( + vectors=vectors, + hidden_dim=64, + k=4, + epochs=2, + batch_size=4, + device_name="CPU", + output_path=weights_path, + ) + manager = SAEManager(device="cpu", weights_path=weights_path) + assert manager.load_model() + assert manager.is_trained() + + +def test_manager_not_trained_raises(): + manager = SAEManager(device="cpu") + assert not manager.is_trained() + with pytest.raises(ValueError, match="not trained"): + manager.encode_vectors(np.random.randn(2, 768).astype(np.float32)) diff --git a/backend/tests/test_static_dist.py b/backend/tests/test_static_dist.py new file mode 100644 index 0000000000000000000000000000000000000000..b0cc95f7dba76d5486a8f36eec49c11066dd2b7e --- /dev/null +++ b/backend/tests/test_static_dist.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from pathlib import Path + +from backend.static_dist import resolve_dist_file + + +def test_resolve_exact_file(tmp_path: Path): + (tmp_path / "assets").mkdir() + asset = tmp_path / "assets" / "app.js" + asset.write_text("ok", encoding="utf-8") + assert resolve_dist_file(tmp_path, "assets/app.js") == asset + + +def test_resolve_v25_directory_index(tmp_path: Path): + v25 = tmp_path / "v25" + v25.mkdir() + index = v25 / "index.html" + index.write_text("v25", encoding="utf-8") + (tmp_path / "index.html").write_text("legacy", encoding="utf-8") + + assert resolve_dist_file(tmp_path, "v25") == index + assert resolve_dist_file(tmp_path, "v25/") == index + + +def test_resolve_v25_index_html_exact(tmp_path: Path): + v25 = tmp_path / "v25" + v25.mkdir() + index = v25 / "index.html" + index.write_text("v25", encoding="utf-8") + assert resolve_dist_file(tmp_path, "v25/index.html") == index + + +def test_resolve_unknown_falls_back_to_root_index(tmp_path: Path): + root_index = tmp_path / "index.html" + root_index.write_text("legacy", encoding="utf-8") + assert resolve_dist_file(tmp_path, "missing/route") == root_index diff --git a/backend/tests/test_vocab_merge.py b/backend/tests/test_vocab_merge.py new file mode 100644 index 0000000000000000000000000000000000000000..0283928ac35245d870633430dca28d26f1355b10 --- /dev/null +++ b/backend/tests/test_vocab_merge.py @@ -0,0 +1,64 @@ +"""EN∪ES vocab merge + demo pair presence.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from backend.vocab_merge import ( + DEMO_PAIRS, + DEMO_WORDS, + assert_demo_pairs_present, + merge_en_es_files, + merge_vocab_lists, +) + + +def test_merge_dedupes_and_lowercases(): + merged = merge_vocab_lists( + ["King", "queen", "man"], + ["rey", "QUEEN", "mujer"], + ensure_words=frozenset(), + ) + assert merged == ["king", "queen", "man", "rey", "mujer"] + + +def test_merge_ensures_demo_words(): + merged = merge_vocab_lists(["alpha"], ["beta"], ensure_words=DEMO_WORDS) + assert_demo_pairs_present(merged) + for en, es in DEMO_PAIRS: + assert en in merged + assert es in merged + + +def test_merge_en_es_files_roundtrip(tmp_path: Path): + en = tmp_path / "en.txt" + es = tmp_path / "es.txt" + out = tmp_path / "merged.txt" + en.write_text("king\nwoman\nzzz\n", encoding="utf-8") + es.write_text("rey\nmujer\naaa\n", encoding="utf-8") + merged = merge_en_es_files(en, es, out) + text = out.read_text(encoding="utf-8") + lines = [ln for ln in text.splitlines() if ln.strip()] + assert lines == merged + assert_demo_pairs_present(merged) + assert "zzz" in merged + assert "aaa" in merged + + +def test_assert_demo_pairs_raises_when_missing(): + with pytest.raises(AssertionError, match="missing demo"): + assert_demo_pairs_present(["king", "rey"]) + + +def test_repo_seed_es_contains_demo_spanish(): + root = Path(__file__).resolve().parents[2] + es_path = root / "public" / "vocab_es.txt" + assert es_path.is_file() + words = { + ln.strip().lower() + for ln in es_path.read_text(encoding="utf-8").splitlines() + if ln.strip() and not ln.strip().startswith("#") + } + for _, es in DEMO_PAIRS: + assert es in words, f"vocab_es.txt missing {es}" diff --git a/backend/tests/test_vocab_npz_metadata.py b/backend/tests/test_vocab_npz_metadata.py new file mode 100644 index 0000000000000000000000000000000000000000..485d58c19bc10d50786c0b163e1455d01b5eae28 --- /dev/null +++ b/backend/tests/test_vocab_npz_metadata.py @@ -0,0 +1,153 @@ +"""NPZ metadata roundtrip + selection compatibility (no SentenceTransformer).""" + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import MagicMock + +import numpy as np +import pytest +from backend.model_catalog import ModelSelection, get_model, resolve_profile +from backend.state import AppState +from backend.vocab_embeddings import ( + load_vocab_embeddings_npz, + npz_compatible_with_selection, + save_vocab_embeddings_npz, +) + + +def test_npz_roundtrip_metadata(tmp_path: Path): + words = ["king", "queen"] + embeddings = np.eye(2, 4, dtype=np.float32) + path = tmp_path / "vocab.npz" + save_vocab_embeddings_npz( + path, + words=words, + embeddings=embeddings, + model_name="Snowflake/snowflake-arctic-embed-m-v2.0", + truncate_dim=4, + ) + cache = load_vocab_embeddings_npz(path) + assert cache.words == words + assert cache.model_name == "Snowflake/snowflake-arctic-embed-m-v2.0" + assert cache.truncate_dim == 4 + assert cache.embedding_dim == 4 + np.testing.assert_allclose(cache.embeddings, embeddings) + + +def test_npz_roundtrip_without_truncate(tmp_path: Path): + path = tmp_path / "vocab.npz" + save_vocab_embeddings_npz( + path, + words=["a"], + embeddings=np.ones((1, 3), dtype=np.float32), + model_name="sentence-transformers/all-mpnet-base-v2", + truncate_dim=None, + ) + cache = load_vocab_embeddings_npz(path) + assert cache.truncate_dim is None + assert cache.embedding_dim == 3 + + +def test_npz_compatible_matching_profile(): + sel = resolve_profile("local-full") + cache_words = ["x"] + emb = np.zeros((1, 256), dtype=np.float32) + from backend.vocab_embeddings import VocabEmbeddingsCache + + cache = VocabEmbeddingsCache( + words=cache_words, + embeddings=emb, + model_name=sel.hub_id, + truncate_dim=256, + embedding_dim=256, + ) + ok, reason = npz_compatible_with_selection(cache, sel) + assert ok is True + assert reason is None + + +def test_npz_incompatible_wrong_model(): + sel = get_model("sentence-transformers/all-mpnet-base-v2") + from backend.vocab_embeddings import VocabEmbeddingsCache + + cache = VocabEmbeddingsCache( + words=["x"], + embeddings=np.zeros((1, 768), dtype=np.float32), + model_name="intfloat/multilingual-e5-small", + truncate_dim=None, + embedding_dim=768, + ) + ok, reason = npz_compatible_with_selection(cache, sel) + assert ok is False + assert reason is not None + assert "model_name" in reason + + +def test_npz_incompatible_wrong_dim(): + sel = resolve_profile("local-full") # truncate 256 + from backend.vocab_embeddings import VocabEmbeddingsCache + + cache = VocabEmbeddingsCache( + words=["x"], + embeddings=np.zeros((1, 768), dtype=np.float32), + model_name=sel.hub_id, + truncate_dim=768, + embedding_dim=768, + ) + ok, reason = npz_compatible_with_selection(cache, sel) + assert ok is False + assert reason is not None + + +def test_appstate_rebuilds_npz_on_mismatch( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +): + vocab = tmp_path / "vocab.txt" + vocab.write_text("king\nqueen\n", encoding="utf-8") + stale = tmp_path / "vocab_embeddings.npz" + save_vocab_embeddings_npz( + stale, + words=["king", "queen"], + embeddings=np.eye(2, 8, dtype=np.float32), + model_name="wrong-model", + truncate_dim=8, + ) + + sel = ModelSelection( + hub_id="sentence-transformers/all-mpnet-base-v2", + profile=None, + trust_remote_code=False, + e5_mode=False, + truncate_dim=None, + gated=False, + short_label="EN baseline (mpnet)", + ) + + fake_model = MagicMock() + fake_model.get_sentence_embedding_dimension.return_value = 4 + + def fake_encode(batch, **kwargs): + n = len(batch) if not isinstance(batch, str) else 1 + return np.ones((n, 4), dtype=np.float32) + + fake_model.encode.side_effect = fake_encode + + monkeypatch.setattr( + "backend.state.build_model", lambda selection, device: fake_model + ) + monkeypatch.setattr("backend.device.get_optimal_device", lambda env: "cpu") + + app = AppState() + app.load_model_and_vocab( + vocab_path=str(vocab), + vocab_embeddings_path=str(stale), + selection=sel, + device_env="CPU", + ) + assert app.is_loaded + assert app.vocab_embeddings is not None + assert app.vocab_embeddings.shape == (2, 4) + rebuilt = load_vocab_embeddings_npz(stale) + assert rebuilt.model_name == sel.hub_id + assert rebuilt.embedding_dim == 4 diff --git a/backend/uv.lock b/backend/uv.lock new file mode 100644 index 0000000000000000000000000000000000000000..7709003859e3aa83e3fe39c0fb2a2b8dbe60dc7b --- /dev/null +++ b/backend/uv.lock @@ -0,0 +1,1809 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'linux'", +] + +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fastapi" +version = "0.141.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, +] + +[[package]] +name = "filelock" +version = "3.32.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/57/3ba6e6cb097f85b855b00163d169f35365f44277df044dcf96d55b8f62a3/filelock-3.32.2.tar.gz", hash = "sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8", size = 217172, upload-time = "2026-07-29T22:46:04.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/e8/72f8cef9fdfeffe06213fe8508039396ee48daa0e3259457ed766173bfd6/filelock-3.32.2-py3-none-any.whl", hash = "sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82", size = 98830, upload-time = "2026-07-29T22:46:03.52Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/78/f34251dadb8f3921264a1d9b8946f5e542014ee2614b285261b4e40e6775/fsspec-2026.7.0.tar.gz", hash = "sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88", size = 317040, upload-time = "2026-07-28T16:34:51.052Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/3c/6a2bf344106328fd04963664a60b9bb6496fc25df8e962fcdc1367285fb9/fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279", size = 206583, upload-time = "2026-07-28T16:34:49.538Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/39/67be8d71f900d9a55761b6022821d6679fb56c64f1b6063d5af2c2606727/hf_xet-1.5.2.tar.gz", hash = "sha256:73044bd31bae33c984af832d19c752a0dffb67518fee9ddbd91d616e1101cf47", size = 903674, upload-time = "2026-07-16T17:29:56.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/be/525eabac5d1736b679c39e342ecd4292534012546a2d18f0043c8e3b6021/hf_xet-1.5.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:4a5ecb9cda8512ba2aa8ee5d37c87a1422992165892d653098c7b90247481c3b", size = 4064284, upload-time = "2026-07-16T17:29:29.907Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3f/699749dd78442480eda4e4fca494284b0e3542e4063cc37654d5fdc929e6/hf_xet-1.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8764488197c1d7b1378c8438c18d2eea902e150dbca0b0f0d2d32603fb9b5576", size = 3828537, upload-time = "2026-07-16T17:29:31.549Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/2658ac0a5b9f4664ca27ce31bd015044fe9dea50ed455fb5197aba819c11/hf_xet-1.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d7446f72abbf7e01ca5ff131786bc2e74a56393462c17a6bf1e303fbab81db4", size = 4417133, upload-time = "2026-07-16T17:29:33.391Z" }, + { url = "https://files.pythonhosted.org/packages/d9/58/8343f3cb63c8fa058d576136df3871550f7d5214a8f048a7ea2eab6ac906/hf_xet-1.5.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:580e59e29bf37aece1f2b68537de1e3fb04f43a23d910dcf6f128280b5bfbba4", size = 4212613, upload-time = "2026-07-16T17:29:34.989Z" }, + { url = "https://files.pythonhosted.org/packages/0c/33/a968f4e4535037b36941ec00714625fb60e026302407e7e26ca9f3e65f4e/hf_xet-1.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bee28c619622d36968056532fd49cf2b35ca75099b1d616c31a618a893491380", size = 4412710, upload-time = "2026-07-16T17:29:36.646Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/9e33981173dbaf194ba0015202b02d467b624d44d4eba89e1bf06c0d2995/hf_xet-1.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e396ab0faf6298199ad7a95305c3ca8498cb825978a6485be6d00587ee4ec577", size = 4628455, upload-time = "2026-07-16T17:29:38.352Z" }, + { url = "https://files.pythonhosted.org/packages/e9/4b/cc682832de4264a03880a2d1b5ec3e1fab3bf307f508817250baafdb9996/hf_xet-1.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fd3add255549e8ef58fa35b2e42dc016961c050600444e7d77d030ba6b57120e", size = 3979044, upload-time = "2026-07-16T17:29:40.329Z" }, + { url = "https://files.pythonhosted.org/packages/ea/09/b2cdf2a0fb39a08af3222b96092a36bd3b40c54123eef07de4422e870971/hf_xet-1.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d6f9c58549407b84b9a5383afd68db0acc42345326a3159990b36a5ca8a20e4e", size = 3808037, upload-time = "2026-07-16T17:29:42.357Z" }, + { url = "https://files.pythonhosted.org/packages/de/ba/2b70603c7552db82baeb2623e2336898304a17328845151be4fe1f48d420/hf_xet-1.5.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f922b8f5fb84f1dd3d7ab7a1316354a1bca9b1c73ecfc19c76e51a2a49d29799", size = 4033760, upload-time = "2026-07-16T17:29:43.884Z" }, + { url = "https://files.pythonhosted.org/packages/60/ac/b097a86a1e4a6098f3a79382643ab09d5733d87ccc864877ad1e12b49b70/hf_xet-1.5.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:045f84440c55cdeb659cf1a1dd48c77bcd0d2e93632e2fea8f2c3bdee79f38ed", size = 3841438, upload-time = "2026-07-16T17:29:45.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/35/db860aa3a0780660324a506ad4b3d322ddc6ecbba4b9340aed0942cbf21c/hf_xet-1.5.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db78c39c83d6279daddc98e2238f373ab8980685556d42472b4ec51abcf03e8c", size = 4428006, upload-time = "2026-07-16T17:29:46.996Z" }, + { url = "https://files.pythonhosted.org/packages/af/6b/832dd980af4b0c3ae0660e309285f2ffcdff2faa38129390dbb47aa4a3f9/hf_xet-1.5.2-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7db73c810500c54c6760be8c39d4b2e476974de85424c50063efc22fdda13025", size = 4221099, upload-time = "2026-07-16T17:29:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/9e/05/ae50f0d34e3254e6c3e208beb2519f6b8673016fc4b3643badaf6450d186/hf_xet-1.5.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6395cfe3c9cbead4f16b31808b0e67eac428b66c656f856e99636adaddea878f", size = 4420766, upload-time = "2026-07-16T17:29:50.092Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/c050bc2743a2bcd68928bfee157b08681667a164a24ec95fbfcfcd717e08/hf_xet-1.5.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cde8cd167126bb6109b2ceb19b844433a4988643e8f3e01dd9dd0e4a34535097", size = 4636716, upload-time = "2026-07-16T17:29:51.62Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/68b01c5c2edb56ac9a67b3d076ffddcb90867abaee923923eb34e7a14e76/hf_xet-1.5.2-cp38-abi3-win_amd64.whl", hash = "sha256:ecf63d1cb69a9a7319910f8f83fcf9b46e7a32dfcf4b8f8eeddb55f647306e65", size = 3988373, upload-time = "2026-07-16T17:29:53.395Z" }, + { url = "https://files.pythonhosted.org/packages/39/c6/988383e9dc17294d536fcbcd6fd16eed882e411ad16c954984a53e47b09c/hf_xet-1.5.2-cp38-abi3-win_arm64.whl", hash = "sha256:1da28519496eb7c8094c11e4d25509b4a468457a0302d58136099db2fd9a671d", size = 3816957, upload-time = "2026-07-16T17:29:54.991Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/db/3582597f8be0d34bd6881365a26d390854f12893eabdd62dd36de9df5a47/huggingface_hub-1.26.0.tar.gz", hash = "sha256:c8cd4e2df1ba9402f77fce9b509ec1d52debb502551789473f34016acc14e361", size = 936665, upload-time = "2026-07-30T14:12:04.156Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/bb/63a644c75b545f3ff394b822e9bd1c4a9586489c618b77a4d8a44a33a23b/huggingface_hub-1.26.0-py3-none-any.whl", hash = "sha256:e8cca670caa5d8dfa7e45bf45e86b466698198cd8150c021bcdb4a86b9252364", size = 780357, upload-time = "2026-07-30T14:12:01.998Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.48.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/a0/acc8ffcd5bdc63df0097e22c719bfcd61b604358343089313a8aebbb24ab/llvmlite-0.48.0.tar.gz", hash = "sha256:543b19f9ef8f3c7c60d1468191e4ee1b1537bf9f8a3d56f64c0ddd98de92edd2", size = 184016, upload-time = "2026-07-02T20:20:05.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/4e/32543c42568fb321b3bdfcf9106e4116ab8f5a7bbcfd9ecf5569b0c07d83/llvmlite-0.48.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:614aad57df707e3172efd5165f2aa7da6a0c6897e40dce590bf756396815ba76", size = 40480650, upload-time = "2026-07-01T18:41:01.945Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0d/6aa48abd423067139a129d1434b77bbcc56080db51d12a88510bb491ca3d/llvmlite-0.48.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:13532f248960ba888ad5ab8150494e2f3a3d20e5f59f264e63741ea5b0ba844c", size = 59890118, upload-time = "2026-07-01T18:41:10.608Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c7/aa917444d871a79608af49149de1b28764e87d2ab41f933c5cd02431d03d/llvmlite-0.48.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ee0c77685a18f5fca994ae21d0763007fca5c5c64b41de37accc78b69079176", size = 58343459, upload-time = "2026-07-01T18:41:06.21Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2b/ceee1cdc263617109d514ac4d1b31f10a282662740ff7d5777baae25b3b5/llvmlite-0.48.0-cp310-cp310-win_amd64.whl", hash = "sha256:02853fe4214acb3780fc920c3fee10564b61d58a35e1b78afcc8a546c2deaba3", size = 41864734, upload-time = "2026-07-01T18:41:14.746Z" }, + { url = "https://files.pythonhosted.org/packages/9a/55/595981f14fbae9ba966feb12af552b1fe69889e44e64ac883a731ed335e0/llvmlite-0.48.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:56a7e24607d3f02d7b1bae8d29c7e1e423d53143d68b072999777f19678fe77b", size = 40480651, upload-time = "2026-07-01T18:41:18.438Z" }, + { url = "https://files.pythonhosted.org/packages/26/08/0109d1b9cb3f4603f3890e30bc66c65332b79185f12a045343b2ae431f67/llvmlite-0.48.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6fa532d6bb3fd3f0803567c736401c54aecfe1a396d3ad25d2440d220e09f0e7", size = 59890118, upload-time = "2026-07-01T18:41:28.184Z" }, + { url = "https://files.pythonhosted.org/packages/02/eb/c5281be180c789cdffbf45b671884c57d7e61345ef3b0f643a4965e108e8/llvmlite-0.48.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:979a66a3f28a02565383ff463527dce78e9b856298872a361283132488e83591", size = 58343458, upload-time = "2026-07-01T18:41:23.397Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f7/b3222b13f2d424dae3c9e63fde476af25ebccf1f3faf0b52d1b79fc15c70/llvmlite-0.48.0-cp311-cp311-win_amd64.whl", hash = "sha256:efaee0276e5e17c2b99b92e0c974bd484ef5977cf5dbc9168e82b71578edb47f", size = 41864734, upload-time = "2026-07-01T18:41:31.932Z" }, + { url = "https://files.pythonhosted.org/packages/92/a2/28696a9e61e245d1a79816d29d106692a90a2b6e7d78c98b326db70827af/llvmlite-0.48.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:d66c3beb4209087ddd4cf4ed2a0856b6887e6a913bdcf1aacfec9851cf2cba4e", size = 40480651, upload-time = "2026-07-01T18:41:35.694Z" }, + { url = "https://files.pythonhosted.org/packages/80/f2/72409351db66d0a317ec5087e076f31fb7b773a640db8a90ce6b5cac9edd/llvmlite-0.48.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:416fa4c2c66c2c6dc6d0a402648c19206e548efa0aa1eff01ad5cdad0af8217d", size = 59890118, upload-time = "2026-07-01T18:41:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/3a/27/5ae2f3722606360480707adb47f001ad89df8251d06b14ee80336e660b66/llvmlite-0.48.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5e5a5131045b72345c71062ea1a91910dde913792b6c9b28ebb2c1c0a712e98", size = 58343459, upload-time = "2026-07-01T18:41:40.306Z" }, + { url = "https://files.pythonhosted.org/packages/16/78/d824ffff7521cd140dc2006e44ce2bc82e64b48d1b32e90e956308c85a74/llvmlite-0.48.0-cp312-cp312-win_amd64.whl", hash = "sha256:d45c7541a80934ec6d8ab0defe67439494ecd2193cbf852a44ba827808976ac1", size = 41865022, upload-time = "2026-07-01T18:41:48.663Z" }, + { url = "https://files.pythonhosted.org/packages/9c/23/fe9316d14626b42c73ef0b502e724705a6ee9450afe53759c0a99c37c2d7/llvmlite-0.48.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a83a99ef0c05b4ccddf9b6218ed9fe84b653a0caf7c1d9dbe148d6d16c67f518", size = 40480652, upload-time = "2026-07-01T18:41:52.216Z" }, + { url = "https://files.pythonhosted.org/packages/1b/4a/90715fa12006d681270b08d881195b6fab3ec39572e048764a1f7f59fed7/llvmlite-0.48.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8761b9e522f55207e24424fcd98370289eec2710bf8e915c82d1053f642450dc", size = 59890120, upload-time = "2026-07-01T18:42:00.748Z" }, + { url = "https://files.pythonhosted.org/packages/70/5e/7b3e20d64650ca3c80af0cdb664ec4b575ec83d9d4dd05bea8bd31f9bbb6/llvmlite-0.48.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fe5cb59b2063bfa039dcb8ca6481c0181bf552f340d10dcf61d7996a665556e", size = 58343457, upload-time = "2026-07-01T18:41:56.41Z" }, + { url = "https://files.pythonhosted.org/packages/17/97/5a430055d1838cf1fb7a01cfa943300f5e4c026fc6333a522c5e4a03b0c1/llvmlite-0.48.0-cp313-cp313-win_amd64.whl", hash = "sha256:91c7e24e74cde3f02b88aa5acca678373f9e069f3b98531b3dbb3a142d9d10bb", size = 41865022, upload-time = "2026-07-01T18:42:04.57Z" }, + { url = "https://files.pythonhosted.org/packages/8d/8e/8170f2e0c217f88069c333d85bb976e536b332aecfcce606ddbdb249385f/llvmlite-0.48.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:321f1ac39b462603f0b589751aecf2d237d056f6d005749c1752b6f23ec3f074", size = 40480650, upload-time = "2026-07-01T18:42:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e1/05b50692b647cac3c18200ac485b04f342f00ed173c9cc46767274469a15/llvmlite-0.48.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:05f0103c8f2f96a37441337e3643863c01b8e83e530aff38960dcb383c54a065", size = 59890115, upload-time = "2026-07-01T18:42:17.805Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c3/470b8c4ff9ae2db2f9cf5c3e73de76ed908a32788ae9eb5602d43e6a476b/llvmlite-0.48.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37d66fae72802175b0bfe1ea06e624b51e2d7aee6c3c34bbd09739b8f88e8e0b", size = 58343457, upload-time = "2026-07-01T18:42:13.217Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2d/6a5171fb7236ac0895e1a02ccba3735bf291e8597239aa6421894d3c0ba8/llvmlite-0.48.0-cp314-cp314-win_amd64.whl", hash = "sha256:966dcab0a598e2bd8fb5f2cc082cf7b07bae564fc485a3a8692393caf986facf", size = 42986372, upload-time = "2026-07-01T18:42:21.483Z" }, + { url = "https://files.pythonhosted.org/packages/94/e3/7a93e09c9f94e637ca90209ceef0334a9a1d45b0bdb7c92ff922d25d6187/llvmlite-0.48.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a5c413317050a1d67c34708bde97707f9b2257ef1017f7532d21fe7d9a9ff30", size = 40480654, upload-time = "2026-07-01T18:42:25.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/98/a29133b4728671a175f7d616fab8b1c6e1d8c269d1523581d3160697bfb1/llvmlite-0.48.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d9f952dff6c350c529997423d4fa43abae9722a884ac7ffafc37a3af676e7db", size = 59890119, upload-time = "2026-07-01T18:42:33.88Z" }, + { url = "https://files.pythonhosted.org/packages/1a/cf/7aac11a1f1c7ec54b60c7f6814e87561fb6b55b2f290455d7941eb113420/llvmlite-0.48.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:054aa7d46595935565f276cf0c1659b4f10929c996dd4a606875fae26fba2a23", size = 58343460, upload-time = "2026-07-01T18:42:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/db/41/b96f440c7df5ebba07872cad4e30fbc3560387755b1ea0b629adb76d5ca8/llvmlite-0.48.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d0b3c61aac83b42fb48cc96bffbf57c81b82b2aa92276b7ed6420c814629a99a", size = 42986383, upload-time = "2026-07-01T18:42:37.544Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "narwhals" +version = "2.24.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/1d/58946e5aab18393e793bd4add6985b95d0e01c3a2d832f38f54468b10dcd/narwhals-2.24.0.tar.gz", hash = "sha256:b5c0f684ccd9d7475b564111e319a4964abcf2baf79d3cf6b1003d06ac9b828d", size = 661143, upload-time = "2026-07-13T10:49:19.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/85/a5bfaebfd305ac18b57b0854d74e37e586809061a91fda62f0bd50c8518e/narwhals-2.24.0-py3-none-any.whl", hash = "sha256:42fdedf44e5b2ca7505630d45b4ac3058f38d8485cba9fe1652ca23152df7489", size = 461030, upload-time = "2026-07-13T10:49:17.571Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'linux'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'linux'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numba" +version = "0.66.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/a0/570e3dc53e5602b49108f62a13e529f1eec8bfc7ef37d49c825924dcf546/numba-0.66.0.tar.gz", hash = "sha256:b900e63a0e26c05ea9a6d5a3a5a0a177cb64c5011887bf43edb8c3ed2c38d363", size = 2806181, upload-time = "2026-07-01T23:12:46.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/48/d139bde40f2359351bfe26ee1b261937f458ac177ab810d4f045ae1c9d92/numba-0.66.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:27951c47e0def9bf8afe580eb961102902e2fd23cb77924b7d9d7cc0f8b444cb", size = 2727368, upload-time = "2026-07-01T23:12:04.282Z" }, + { url = "https://files.pythonhosted.org/packages/36/e4/b780bfa9191410da50ba249cb3248a75014e17f611e72709cbddcb21f42d/numba-0.66.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc408c54b450f41582f4be1608f8981c1dcc44c7f40355cc150dd93015753407", size = 3803554, upload-time = "2026-07-01T23:12:06.379Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b2/a051b96626bdf5c4d8fa6b8d450605c09638d85dc872ab63ef9a67096dca/numba-0.66.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c14c044c06b453ec3fa7715dfe75425e2ba72c73377a7ffde6d9ec511dfd94c", size = 3510065, upload-time = "2026-07-01T23:12:08.051Z" }, + { url = "https://files.pythonhosted.org/packages/34/01/24dcdc3e919522e2efbd92969c281ff40deb1d5f8a994bcd0057081c158c/numba-0.66.0-cp310-cp310-win_amd64.whl", hash = "sha256:2338cc0d43609fe448930848fd35a5bc688761b986f81b597a6f45cc0f8c9577", size = 2780379, upload-time = "2026-07-01T23:12:09.772Z" }, + { url = "https://files.pythonhosted.org/packages/9e/02/970796b4daa709604cde22e87a7cda9bde473c278ea4a75f59fe38cee47f/numba-0.66.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:bbd531c327557a9004507fa6bff06c53ab51a7a5776b75261bb9cef1efe2b2ea", size = 2727049, upload-time = "2026-07-01T23:12:11.296Z" }, + { url = "https://files.pythonhosted.org/packages/8c/99/33a6ed9c1a0b5e42efa98eb0edf617d61dca576c82625947377b1d4540c9/numba-0.66.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc6629becb21a867d85401ec89f426dd24c484a4193ade8a38309debfd1529ca", size = 3808870, upload-time = "2026-07-01T23:12:12.944Z" }, + { url = "https://files.pythonhosted.org/packages/04/20/8c51126025211659235b8de2866dfa226984ae0c8273461a3cf374716741/numba-0.66.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aac69f3ccb8af100f5913c1241edc9692bad1cdd2508721713f426eb06c9a659", size = 3514498, upload-time = "2026-07-01T23:12:15.307Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c9/9476940bc6d5caf5c0cf2e4c5feecbf01244bbe6f914614082dd7a3e520e/numba-0.66.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb601841d9e02e6237bb6522e36d0741614be3cfe2b482a6f00a41b5ba209443", size = 2780225, upload-time = "2026-07-01T23:12:16.924Z" }, + { url = "https://files.pythonhosted.org/packages/62/a3/70deb7f88461c1cd5d16aa990c2380604102661a427667b8950dcdccc27f/numba-0.66.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:53ca5900b7cab15109796030113a6b28576bae5ad7bb507ad6dd1360ddd81ba4", size = 2727264, upload-time = "2026-07-01T23:12:18.669Z" }, + { url = "https://files.pythonhosted.org/packages/2d/55/25c319845e9a4e08f16611ddbda56a192eb7b6ed13e1a2bff2da272ffb97/numba-0.66.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0999e3ee1b18c48e1fb51d11af35ef59852c7f4f50569c9550c25faef0616ad1", size = 3866252, upload-time = "2026-07-01T23:12:20.429Z" }, + { url = "https://files.pythonhosted.org/packages/71/ef/a82d6fd6bf1b0fe461651e924d3647eeec9ac17f8eee4896264bf7480930/numba-0.66.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efe0d2d5099790df945e0cb6e1b3104bd965d7bbfac50d62f1d5d1d6ade0825d", size = 3566974, upload-time = "2026-07-01T23:12:22.116Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/9e6171e378822ab191c7abcfd3d8cfc8644516f6c7834c22e210e4acc070/numba-0.66.0-cp312-cp312-win_amd64.whl", hash = "sha256:b075a4e7ebc43dc6294f223e2821659656209fd5e0ce53245877c23d66d6e1a9", size = 2797403, upload-time = "2026-07-01T23:12:23.724Z" }, + { url = "https://files.pythonhosted.org/packages/03/52/176c02d005c5c5143cde10a85bbcdcb6236d9e34c3aac089380e0506cd1d/numba-0.66.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:380b2556a2019ccd1e956ae77dd257eaa39403f7520768b626d44b755112785e", size = 2727084, upload-time = "2026-07-01T23:12:25.434Z" }, + { url = "https://files.pythonhosted.org/packages/44/b5/e930010965568fe7f2c6c962fd2849d458cb9f62c3ab7584af8a19a2b40a/numba-0.66.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:939316d5d8619751207b8972a67852b5a7646665298cb4de693cd6bf135152f4", size = 3873663, upload-time = "2026-07-01T23:12:27.308Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ec/5b51457cbe96e4831141d83e892e65191b23a1b78728456c62909d231ace/numba-0.66.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdf506775d9f02eb92a87bf5c5b1e0d25506fd18cafd769f4ed914a8feac73e7", size = 3573529, upload-time = "2026-07-01T23:12:28.944Z" }, + { url = "https://files.pythonhosted.org/packages/83/7e/cea7710e96913d3c7f2999f16db1b28e6c5be5171cbf40f77f98333a7243/numba-0.66.0-cp313-cp313-win_amd64.whl", hash = "sha256:c5bfe5350284509ab0474390321454c3a8627a188af5b68c910e83df3e2db4a7", size = 2797247, upload-time = "2026-07-01T23:12:30.774Z" }, + { url = "https://files.pythonhosted.org/packages/96/7a/7e0e73550eb4e41ede6e72fb5371f4539537a4d770a3b73fa9b61aea0622/numba-0.66.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:46ae5f2b19e2af3c33c2df100306a90ea2f981c8158b0390f8bf6c20eee7357e", size = 2727296, upload-time = "2026-07-01T23:12:32.39Z" }, + { url = "https://files.pythonhosted.org/packages/0f/26/885774c006de6620ed3d10f45d8e20fe0b8e6aad6d573211a2cbc8b3e528/numba-0.66.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e2b101f23b8b63978d334574d2039f27f0dccfe1d891756f33a2e2f3e4c88cf4", size = 3842720, upload-time = "2026-07-01T23:12:33.938Z" }, + { url = "https://files.pythonhosted.org/packages/93/99/edebf7de890b73973d839dd971cf73734adfb81ffa1b4504f84b9059c3e5/numba-0.66.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63b943eb2c9ba371908ce2cd6dfc643db51fc40f7966993376a1701bc922f537", size = 3543537, upload-time = "2026-07-01T23:12:35.566Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/b46ad28ac3681d035ea21365c5e052149062e1a0a9affd0563d2760ea6ff/numba-0.66.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd57790acd20f6a468e0ad333ef6b82355e309a92310fb7dff80e919f01a21a9", size = 2799250, upload-time = "2026-07-01T23:12:37.154Z" }, + { url = "https://files.pythonhosted.org/packages/10/6f/5e77a7397a37dd16f57a7b72e7e470db5227b68e3639df0d13a8e674883d/numba-0.66.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:db7735d15ea17a283d6485b9fa3504769f78fd86e5146638ad5e8da57c031b9e", size = 2730342, upload-time = "2026-07-01T23:12:38.758Z" }, + { url = "https://files.pythonhosted.org/packages/39/fd/e9c9680a3813f3d781c20e5d53c1074801b787d4feecca0472fdd7c05ce1/numba-0.66.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:651a2b53298340956db26ecbe7ab043106b50a40c2807e66d617a4917245a4ab", size = 3878695, upload-time = "2026-07-01T23:12:40.302Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/9b363287b85fcd4537ea3878793822878b2ac1008a78159d2096fea628de/numba-0.66.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c1144ba1720ea59ad79f4f488ed54d149b2613b357f7e445678b7d0739c70e9", size = 3596323, upload-time = "2026-07-01T23:12:42.805Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f2/dca53d50b8f2289dd01954ace9da261e0487d5b74b188b4304e4ecc3492c/numba-0.66.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d426178fb991a85714c43112a8ea7b9d9579ea856ad8dcdb9c1c3941903ba5be", size = 2804772, upload-time = "2026-07-01T23:12:44.399Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'linux'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'linux'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/5d/b95ca542a001135cc250a49370f282f578c8f4e46cc8617d73775297eea8/orjson-3.11.9-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:135869ef917b8704ea0a94e01620e0c05021c15c52036e4663baffe75e72f8ce", size = 228986, upload-time = "2026-05-06T15:09:14.765Z" }, + { url = "https://files.pythonhosted.org/packages/80/01/be33fbff646e22f93398429ea645f20d2097aea1a6cdc1e6628e70125f83/orjson-3.11.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115ab5f5f4a0f203cc2a5f0fb09aee503a3f771aa08392949ab5ca230c4fbdbd", size = 132558, upload-time = "2026-05-06T15:09:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/4e/61/73d49333bba660a075daccca10970dc6409ce1cf42ae4046646a19468aad/orjson-3.11.9-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4da3c38a2083ca4aaf9c2a36776cce3e9328e6647b10d118948f3cfb4913ffe4", size = 128213, upload-time = "2026-05-06T15:09:18.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/7d/30e844b3dac3f74aed66b1f984daf9db3c98c0328c03d965a9e8dc06449e/orjson-3.11.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53b50b0e14084b8f7e29c5ce84c5af0f1160169b30d8a6914231d97d2fe297d4", size = 135430, upload-time = "2026-05-06T15:09:20.257Z" }, + { url = "https://files.pythonhosted.org/packages/16/64/bd815f5c610b3facc204f26ba94e87a9eb49b0d83de3d5fc1eee2402d91b/orjson-3.11.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:231742b4a11dad8d5380a435962c57e91b7c37b79be858f4ef1c0df1a259897e", size = 146178, upload-time = "2026-05-06T15:09:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/c7/35/e744fd36c79b339d27beb06068b5a08a8882ef5418804d0ce545a31f718d/orjson-3.11.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34fd2317602587321faab75ab76c623a0117e80841a6413654f04e47f339a8fb", size = 133068, upload-time = "2026-05-06T15:09:23.228Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/d54152b67b63a0b3e556cfc549d6ce84f74d7f425ddeadc6c8a74d913da7/orjson-3.11.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71f3db16e69b667b132e0f305a833d5497da302d801508cbb051ed9a9819da47", size = 134217, upload-time = "2026-05-06T15:09:24.847Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ee/66154baf69f71c7164a268a5e888908aec5a0819d13c81d5e2755a257758/orjson-3.11.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0b34789fa0da61cf7bef0546b09c738fb195331e017e477096d129e9105ab03d", size = 141917, upload-time = "2026-05-06T15:09:26.647Z" }, + { url = "https://files.pythonhosted.org/packages/09/d3/c5824260ca8b9d7ba82648d042a3f8f4815d18c15bb98a1f30edd1bb2d83/orjson-3.11.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:87e4d4ab280b0c87424d47695bec2182caf8cfc17879ea78dab76680194abc13", size = 415356, upload-time = "2026-05-06T15:09:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/64/cb/509c2e816fe4df641d93dc92f6a89adc8df3ada8ebdee2bd44aba3264c3c/orjson-3.11.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ace6c58523302d3b97b6ac5c38a5298a54b473762b6be82726b4265c41029f92", size = 148112, upload-time = "2026-05-06T15:09:29.783Z" }, + { url = "https://files.pythonhosted.org/packages/db/b5/3ceae56d2e4962979eedb023ba6a46a4bb65f333960379be0ca470686220/orjson-3.11.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:97d0d932803c1b164fde11cb542a9efcb1e0f63b184537cca65887147906ff48", size = 137112, upload-time = "2026-05-06T15:09:31.432Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7a/81fa3f2c7bef79b04cf2ab7838e5ac74b1f12511ceab979759b0275d6bb4/orjson-3.11.9-cp310-cp310-win32.whl", hash = "sha256:b3afcf569c15577a9fe64627292daa3e6b3a70f4fb77a5df246a87ec21681b94", size = 131706, upload-time = "2026-05-06T15:09:32.707Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/b64600f9083c7f151ad39717a5877fccbeb0ef6d7efcb55f971ce00b6bee/orjson-3.11.9-cp310-cp310-win_amd64.whl", hash = "sha256:8697ab6a080a5c46edaad50e2bc5bd8c7ca5c66442d24104fa44ec74910a8244", size = 127282, upload-time = "2026-05-06T15:09:33.955Z" }, + { url = "https://files.pythonhosted.org/packages/1e/51/3fb9e65ae76ee97bd611869a503fa3fc0a6e81dd8b737cf3003f682df7ff/orjson-3.11.9-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f", size = 228522, upload-time = "2026-05-06T15:09:35.362Z" }, + { url = "https://files.pythonhosted.org/packages/16/fa/9d54b07cb3f3b0bfd57841478e42d7a0ece4a9f49f9907eecf5a45461687/orjson-3.11.9-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c", size = 128463, upload-time = "2026-05-06T15:09:37.063Z" }, + { url = "https://files.pythonhosted.org/packages/88/b1/6ceafc2eefd0a553e3be77ce6c49d107e772485d9568629376171c50e634/orjson-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5", size = 132306, upload-time = "2026-05-06T15:09:38.299Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/f11311285324a40aab1e3031385c50b635a7cd0734fdaf60c7e89a696f60/orjson-3.11.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c", size = 127988, upload-time = "2026-05-06T15:09:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/9e/85/0ef63bcf1337f44031ce9b91b1919563f62a37527b3ea4368bb15a22e5d7/orjson-3.11.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd", size = 135188, upload-time = "2026-05-06T15:09:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/05/94/b0d27090ea8a2095db3c2bd1b1c96f96f19bbb494d7fef33130e846e613d/orjson-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62", size = 145937, upload-time = "2026-05-06T15:09:42.249Z" }, + { url = "https://files.pythonhosted.org/packages/09/eb/75d50c29c05b8054013e221e598820a365c8e64065312e75e202ed880709/orjson-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877", size = 132758, upload-time = "2026-05-06T15:09:43.945Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/360686f39348aa88827cb6fbf7dc606fd41c831a35235e1abf1db8e3a9e6/orjson-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1", size = 133971, upload-time = "2026-05-06T15:09:45.239Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/3178eb16f3221aeef068b6f1f1ebe05f656ea5c6dffe9f6c917329fe17a3/orjson-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd", size = 141685, upload-time = "2026-05-06T15:09:46.858Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f1/ff2f19ed0225f9680fafa42febca3570dd59444ebf190980738d376214c2/orjson-3.11.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff", size = 415167, upload-time = "2026-05-06T15:09:48.312Z" }, + { url = "https://files.pythonhosted.org/packages/9b/61/863bddf0da6e9e586765414debd54b4e58db05f560902b6d00658cb88636/orjson-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980", size = 147913, upload-time = "2026-05-06T15:09:49.733Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4081492586d75b073d60c5271a8d0f05a0955cabf1e34c8473f6fcd84235/orjson-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2", size = 136959, upload-time = "2026-05-06T15:09:51.311Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bd/70b6ab193594d7abb875320c0a7c8335e846f28968c432c31042409c3c8d/orjson-3.11.9-cp311-cp311-win32.whl", hash = "sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180", size = 131533, upload-time = "2026-05-06T15:09:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/3f/17/1a1a228183d62d1b77e2c30d210f47dd4768b310ebe1607c63e3c0e3a71e/orjson-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02", size = 127106, upload-time = "2026-05-06T15:09:54.204Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/285de5fa296d09681ee9c546cd4a8aeb773b701cf343dc125994f4d52953/orjson-3.11.9-cp311-cp311-win_arm64.whl", hash = "sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697", size = 126848, upload-time = "2026-05-06T15:09:55.551Z" }, + { url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515, upload-time = "2026-05-06T15:09:57.265Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409, upload-time = "2026-05-06T15:09:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213, upload-time = "2026-05-06T15:10:03.515Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" }, + { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" }, + { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025, upload-time = "2026-05-06T15:10:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" }, + { url = "https://files.pythonhosted.org/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470", size = 131606, upload-time = "2026-05-06T15:10:15.791Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be", size = 127101, upload-time = "2026-05-06T15:10:17.129Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624", size = 126736, upload-time = "2026-05-06T15:10:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" }, + { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" }, + { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" }, + { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" }, + { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" }, + { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" }, + { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" }, + { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" }, + { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" }, + { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" }, + { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" }, + { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" }, + { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pynndescent" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "llvmlite" }, + { name = "numba" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/fb/7f58c397fb31666756457ee2ac4c0289ef2daad57f4ae4be8dec12f80b03/pynndescent-0.6.0.tar.gz", hash = "sha256:7ffde0fb5b400741e055a9f7d377e3702e02250616834231f6c209e39aac24f5", size = 2992987, upload-time = "2026-01-08T21:29:58.943Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/e6/94145d714402fd5ade00b5661f2d0ab981219e07f7db9bfa16786cdb9c04/pynndescent-0.6.0-py3-none-any.whl", hash = "sha256:dc8c74844e4c7f5cbd1e0cd6909da86fdc789e6ff4997336e344779c3d5538ef", size = 73511, upload-time = "2026-01-08T21:29:57.306Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "regex" +version = "2026.7.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/98/04b13f1ddfb63158025291c02e03eb42fbb7acb51d091d541050eb4e35e8/regex-2026.7.19.tar.gz", hash = "sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5", size = 416440, upload-time = "2026-07-19T00:19:48.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/13/bbf7d9d1887fe4a3693527c6caa232c197ea9da91f1212e9672eff60329d/regex-2026.7.19-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:555497390743af1a65045fa4527782d10ff5b88970359412baa4a1e628fe393b", size = 494009, upload-time = "2026-07-19T00:16:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/a3/19/783688e75a2bec15d50aec0d5e7e317d363808bc82a6eb6750b897bfcd7b/regex-2026.7.19-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:343a4504e3fb688c47cad451221ca5d4814f42b1e16c0065bde9cbf7f473bd52", size = 295287, upload-time = "2026-07-19T00:16:15.702Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/cefe4f051302ca298d3f3e79ed6dbd933ac84485b9515acdd6a52d70cef7/regex-2026.7.19-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ebee1ee89c39c953baac6924fcde08c5bb427c4057510862f9d7c7bdb3d8665", size = 290633, upload-time = "2026-07-19T00:16:17.182Z" }, + { url = "https://files.pythonhosted.org/packages/ae/1e/1045ca2cabb12e8ec41ad0d138e9f3ff1eb079d30a6f51ccf7d709b44aad/regex-2026.7.19-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:062f8cb7a9739c4835d22bd96f370c59aba89f257adcfa53be3cc209e08d3ae0", size = 785300, upload-time = "2026-07-19T00:16:18.505Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/20e5bca184e90bf1bd187efdb53363f4a7b7b34f01d54ced5740caf104bd/regex-2026.7.19-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1123ef4211d763ee771d47916a1596e2f4915794f7aabdc1adcb20e4249a6951", size = 854079, upload-time = "2026-07-19T00:16:19.909Z" }, + { url = "https://files.pythonhosted.org/packages/09/9b/5a2e59678be3b24aa6a42b2c6d66a48daa212593e9f4096fe7ba577fa9b1/regex-2026.7.19-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6e44c0e7c5664be20aee92085153150c0a7967310a73a43c0f832b7cd35d0dd3", size = 899496, upload-time = "2026-07-19T00:16:21.453Z" }, + { url = "https://files.pythonhosted.org/packages/48/9a/7317f14ed8ed9fd998d1978b4802b07bc4d79216353c435dbcc1ddd1301f/regex-2026.7.19-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98c6ac18480fcdb33f35439183f1d2e79760ab41930309c6d951cb1f8e46694c", size = 793541, upload-time = "2026-07-19T00:16:22.991Z" }, + { url = "https://files.pythonhosted.org/packages/6f/53/833c2db3e274d3c191f4c42fe5bfa358e4c8b617d5d7312d31334965fc46/regex-2026.7.19-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4458124d71339f505bf1fb94f69fd1bb8fa9d2481eebfef27c10ef4f2b9e12f6", size = 785515, upload-time = "2026-07-19T00:16:24.654Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b9/efb2f9fa151d71db09d4015e1fb92fee47416f01c12164836bbd23e2f3c2/regex-2026.7.19-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbf300e2070bb35038660b3be1be4b91b0024edb41517e6996320b49b92b4175", size = 769556, upload-time = "2026-07-19T00:16:26.207Z" }, + { url = "https://files.pythonhosted.org/packages/81/4d/45610c263f8eadb84e4a1fabd904d81d5176226faa9104ef498bf8a8b285/regex-2026.7.19-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b2b506b1788df5fecd270a10d5e70a95fe77b87ea2b370a318043f6f5f817ee6", size = 774130, upload-time = "2026-07-19T00:16:27.786Z" }, + { url = "https://files.pythonhosted.org/packages/40/95/1b40d87c7a9e5480bec7a87bce9fd67fc3f14b5f106c8ee66d660249072f/regex-2026.7.19-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:52579c60a6078be70a0e49c81d6e56d677f34cd439af281a0083b8c7bc75c095", size = 848694, upload-time = "2026-07-19T00:16:29.412Z" }, + { url = "https://files.pythonhosted.org/packages/17/8b/bb45968addd5b394ef9cd9184bd9c65ade1a819dbb2b92b71ad52a0c7907/regex-2026.7.19-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:2955907b7157a6660f27079edf7e0229e9c9c5325c77a2ef6a890cba91efa6f0", size = 758505, upload-time = "2026-07-19T00:16:31.006Z" }, + { url = "https://files.pythonhosted.org/packages/bf/6f/33386c672fbf43e21602135a0f29a97ee251a483f007fe51d10e9b2dbc93/regex-2026.7.19-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:89dfee3319f5ae3f75ebd5c2445a809bb320252ba5529ffdafea4ef25d79cf1a", size = 836985, upload-time = "2026-07-19T00:16:32.459Z" }, + { url = "https://files.pythonhosted.org/packages/22/f1/9112b86e9bb075619862e8e42b604794389f1958faa68fb69bde505dd90e/regex-2026.7.19-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d3143f159261b1ce5b24c261c590e5913370c3200c5e9ebbb92b5aa5e111902", size = 782610, upload-time = "2026-07-19T00:16:33.857Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f9/13d460d8a385ca0b0be9e6be80a90968b9293b3e30895543ad2d1d1653e4/regex-2026.7.19-cp310-cp310-win32.whl", hash = "sha256:64729333167c2dcaaa56a331d40ee097bd9c5617ffd51dabb09eaddafb1b532e", size = 266772, upload-time = "2026-07-19T00:16:35.194Z" }, + { url = "https://files.pythonhosted.org/packages/9f/90/29addd7a03e1aea402c1f31467e25c80caabc8c3735b88a23cf73b0aa9c2/regex-2026.7.19-cp310-cp310-win_amd64.whl", hash = "sha256:1c398716054621aa300b3d411f467dda903806c5da0df6945ab73982b8d115db", size = 277967, upload-time = "2026-07-19T00:16:36.847Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ba/ecfce06fe66c122bc6f77ae284887a9282e4411fe1e6268c5266611ca054/regex-2026.7.19-cp310-cp310-win_arm64.whl", hash = "sha256:064f1760a5a4ade65c5419be23e782f29147528e8a66e0c42dd4cedb8d4e9fc6", size = 276963, upload-time = "2026-07-19T00:16:38.315Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cef4de2bac939280b68d32adc659478845238a8274f2f79c465063f590ad/regex-2026.7.19-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ac777001cdfc28b72477d93c8564bb7583081ea8fb45cdca3d568e0a4f87183c", size = 494012, upload-time = "2026-07-19T00:16:39.927Z" }, + { url = "https://files.pythonhosted.org/packages/ff/87/e86f51eb117457bb7803132ffe5cb6e2841e2b5bea4cc85d397f3c6e257d/regex-2026.7.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:59787bd5f8c70aa339084e961d2996b53fbdeab4d5393bba5c1fe1fc32e02bae", size = 295281, upload-time = "2026-07-19T00:16:41.433Z" }, + { url = "https://files.pythonhosted.org/packages/41/2e/2360c41d8080a3d9ec7e5c90fad6eab3b50192869d10e9a5609e48c8177b/regex-2026.7.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:90c633e7e8d6bf4e992b8b36ce69e018f834b641dd6de8cea6d78c06ffa119c5", size = 290615, upload-time = "2026-07-19T00:16:43.058Z" }, + { url = "https://files.pythonhosted.org/packages/cf/69/b65ba4344efbc771b28fe5dde84cbbb6c8f9551165952fe78def5b9dde6a/regex-2026.7.19-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ccab0db8d5f4fbb0272642113c1adb2ffc698c16d3a0944580222331fa7a20", size = 791804, upload-time = "2026-07-19T00:16:44.662Z" }, + { url = "https://files.pythonhosted.org/packages/81/b6/a40dfa0dc6224b36f620c00296eacc830489cbf8c2837b6750dfe6170375/regex-2026.7.19-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e50d748a32da622f256e8d505867f5d3c43a837c6a9f0efb149655fadd1042a", size = 861723, upload-time = "2026-07-19T00:16:46.412Z" }, + { url = "https://files.pythonhosted.org/packages/e3/02/735991dee71abd83196a7962f7ed8bf5aa05720ff06e2d3ff896a85e2bbb/regex-2026.7.19-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf1516fe58fc104f39b2d1dbe2d5e27d0cd45c4be2e42ba6ee0cc763701ec3c7", size = 905932, upload-time = "2026-07-19T00:16:47.956Z" }, + { url = "https://files.pythonhosted.org/packages/45/6c/e7098d8b846ccdbf431d8c081b61e496526a27a28094ed09e0dce21b3f54/regex-2026.7.19-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09f3e5287f94f17b709dc9a9e70865855feee835c861613be144218ce4ca82cc", size = 801407, upload-time = "2026-07-19T00:16:49.43Z" }, + { url = "https://files.pythonhosted.org/packages/8a/18/34b69274e2649bcc7d9b089c2b2983fb2632d8ecf667e359593be9072e79/regex-2026.7.19-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6383cd2ed53a646c659ba1fe65727db76437fdaa069e697a0b44a51d5843d864", size = 774448, upload-time = "2026-07-19T00:16:51.352Z" }, + { url = "https://files.pythonhosted.org/packages/bb/e6/0a72247d025585fd3800b98e040b84d562a88af6303347100484849f4f01/regex-2026.7.19-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:09d3007fc76249a83cdd33de160d50e6cb77f54e09d8fa9e7148e10607ce24af", size = 783297, upload-time = "2026-07-19T00:16:53.071Z" }, + { url = "https://files.pythonhosted.org/packages/b1/aa/c4f65ae7dd02a36b323a70c4cff326e1f3442361aaebc9311100a130d54f/regex-2026.7.19-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f8c6e7a1cfa3dc9d0ee2de0e65e834537fa29992cc3976ffec914afc35c5dd5", size = 854736, upload-time = "2026-07-19T00:16:54.607Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/668082bcc817b9e694189b84997aeba7385b7779faa6711788679c482e35/regex-2026.7.19-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b2ea4a3e8357be8849e833beeae757ac3c7a6b3fc055c03c808a53c91ad30d82", size = 763298, upload-time = "2026-07-19T00:16:56.289Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fb/2d07ad555e7af88aa5f867fdafa47a8d945ee237c20af3ebceb46a820835/regex-2026.7.19-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:80115dd39481fd3a4b4080220799dbcacb921a844de4b827264ececacbe17c78", size = 844430, upload-time = "2026-07-19T00:16:57.933Z" }, + { url = "https://files.pythonhosted.org/packages/51/15/c82a471fe3dce56f03745635b43aa456c40dc0db089e07ef148b331507d1/regex-2026.7.19-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6ce43a0269d68cee79a7d1ade7def53c20f8f2a047b92d7b5d5bcc73ae88327", size = 789683, upload-time = "2026-07-19T00:16:59.583Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f4/7532a2c59d56f5398902c20de60f0c9a5d1cd364e42a051b48e1b210be7b/regex-2026.7.19-cp311-cp311-win32.whl", hash = "sha256:9be2a6647740dd3cca6acb24e87f03d7632cd280dbce9bbe40c26353a215a45d", size = 266778, upload-time = "2026-07-19T00:17:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/cf1bc631db154eb95520d9d5dbc2371ff77a0f014bbf7d748fed8496aa63/regex-2026.7.19-cp311-cp311-win_amd64.whl", hash = "sha256:8d3469c91dd92ee41b7c95280edbd975ef1ba9195086686623a1c6e8935ce965", size = 277983, upload-time = "2026-07-19T00:17:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/8d/bd/56ceaf170e875d5a6761bf2bfd0d040f1cacc896850d5e40cb29b11bbd06/regex-2026.7.19-cp311-cp311-win_arm64.whl", hash = "sha256:36aacfb15faaff3ced55afbf35ec72f50d4aee22082c4f7fe0573a33e2fca92e", size = 276961, upload-time = "2026-07-19T00:17:04.135Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b9/d11d7e501ac8fd7d617684423ebb9561e0b998481c1e4cbc0cb212c5d74a/regex-2026.7.19-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d", size = 496778, upload-time = "2026-07-19T00:17:05.677Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a9/a5ab6f312f24318019170dc485d5421fe4f89e43a98640da50d95a8a7041/regex-2026.7.19-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd", size = 297122, upload-time = "2026-07-19T00:17:07.59Z" }, + { url = "https://files.pythonhosted.org/packages/b3/63/4cab4d7f2d384a144d420b763d97674cb70619c878ea6fcd7640d0e62143/regex-2026.7.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6", size = 292009, upload-time = "2026-07-19T00:17:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/22/85/102a81b218298957d4ea7d2f084fae537a71add9d6ff93c8e67284c5f45e/regex-2026.7.19-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797", size = 796708, upload-time = "2026-07-19T00:17:11.542Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/dc136af5629938a037cd2b304c12240e132ec92f38be8ff9cc89af2a1f2d/regex-2026.7.19-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18", size = 865651, upload-time = "2026-07-19T00:17:13.312Z" }, + { url = "https://files.pythonhosted.org/packages/e0/75/67402ae3cd9c8c988a4c805d15ee3eef015e7ca4cb112cf3e640fc1f4153/regex-2026.7.19-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511", size = 911756, upload-time = "2026-07-19T00:17:15.015Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8e/096d00c7c480ef2ff4265349b14e2261d4ab787ba1f74e2e80d1c58079c3/regex-2026.7.19-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68", size = 801798, upload-time = "2026-07-19T00:17:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f0/41/e7ecac6edb5722417f85cc67eaf386322fbe8acf6918ec2fdc37c20dd9d0/regex-2026.7.19-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11", size = 776933, upload-time = "2026-07-19T00:17:19.347Z" }, + { url = "https://files.pythonhosted.org/packages/6f/69/03c9b3f058d66403e0ca2c938696e81d51cd4c6d47ec5265f02f96948d9a/regex-2026.7.19-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986", size = 784338, upload-time = "2026-07-19T00:17:21.057Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f7/b38ab3d43f284afbb618fcd15d0e77eb786ae461ce1f6bc7494619ddc0f2/regex-2026.7.19-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b", size = 860452, upload-time = "2026-07-19T00:17:23.119Z" }, + { url = "https://files.pythonhosted.org/packages/15/5c/ff60ef0571121714f3cf9920bc183071e384a10b556d042e0fdb06cc07a5/regex-2026.7.19-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb", size = 765958, upload-time = "2026-07-19T00:17:24.81Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0f/bd34021162c0ab47f9a315bd56cd5642e920c8e5668a75ef6c6a6fca590d/regex-2026.7.19-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035", size = 851765, upload-time = "2026-07-19T00:17:26.993Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/a2ca43edade0595cccfdc98636739f536d9e26898e7dbddc2b9e98898953/regex-2026.7.19-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a", size = 789714, upload-time = "2026-07-19T00:17:28.699Z" }, + { url = "https://files.pythonhosted.org/packages/5d/47/e02db4015d424fc83c00ea0ac8c5e5ec14397943de9abf909d5ce3a25931/regex-2026.7.19-cp312-cp312-win32.whl", hash = "sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5", size = 267157, upload-time = "2026-07-19T00:17:31.051Z" }, + { url = "https://files.pythonhosted.org/packages/08/8e/c780c131f79b42ed22d1bd7da4096c2c35f813e835acd02ef0f018bd892c/regex-2026.7.19-cp312-cp312-win_amd64.whl", hash = "sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312", size = 277777, upload-time = "2026-07-19T00:17:32.848Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4c/e4d7e086449bdf379d89774bf1f89dc4a41943f3c5a6125a03905b34b5fb/regex-2026.7.19-cp312-cp312-win_arm64.whl", hash = "sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d", size = 277136, upload-time = "2026-07-19T00:17:34.803Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3d/84165e4299ff76f3a40fe1f2abf939e976f693383a08d2beea6af62bd2c1/regex-2026.7.19-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f035d9dc1d25eff9d361456572231c7d27b5ccd473ca7dc0adfce732bd006d40", size = 496552, upload-time = "2026-07-19T00:17:36.808Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/a65293e6e4cf28eb7ee1be5335a5386c40d6742e9f47fafc8fec785e16c7/regex-2026.7.19-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42572142ed0b9d5d261ba727157c426510da78e20828b66bbb855098b8a4e38", size = 296983, upload-time = "2026-07-19T00:17:38.816Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/2d0564e93d87bc48618360ddca232a2ca612bbdf53ce8465d45ca5ce14ee/regex-2026.7.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40b34dd88658e4fedd2fddbf0275ac970d00614b731357f425722a3ed1983d11", size = 291832, upload-time = "2026-07-19T00:17:40.726Z" }, + { url = "https://files.pythonhosted.org/packages/07/cd/42dfbabff3dfc9603c501c0e2e2c5adbb09d127b267bf5348de0af338c15/regex-2026.7.19-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c41c63992bf1874cebb6e7f56fd7d3c007924659a604ae3d90e427d40d4fd13", size = 796775, upload-time = "2026-07-19T00:17:42.382Z" }, + { url = "https://files.pythonhosted.org/packages/df/5d/f6a4839f2b934e3eed5973fd07f5929ee97d4c98939fb275ea23c274ee16/regex-2026.7.19-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d3372064506b94dd2c67c845f2db8062e9e9ba84d04e33cb96d7d33c11fe1ae", size = 865687, upload-time = "2026-07-19T00:17:44.185Z" }, + { url = "https://files.pythonhosted.org/packages/14/b0/b47d6c36049bc59806a50bd4c86ced70bbe058d787f80281b1d7a9b0e024/regex-2026.7.19-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fce7760bf283405b2c7999cab3da4e72f7deca6396013115e3f7a955db9760da", size = 911962, upload-time = "2026-07-19T00:17:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/2a/be/ff61f28f9273658cfe23acbbac5217221f6519960ed401e61dfdab12bc35/regex-2026.7.19-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0d702548d89d572b2929879bc883bb7a4c4709efafe4512cadee56c55c9bd15", size = 801817, upload-time = "2026-07-19T00:17:48.25Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bb/8b4f7f26b333f9f79e1b453613c39bb4776f51d38ae66dd0ba31d6b354ca/regex-2026.7.19-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d446c6ac40bb6e05025ccee55b84d80fe9bf8e93010ffc4bb9484f13d498835f", size = 776908, upload-time = "2026-07-19T00:17:50.183Z" }, + { url = "https://files.pythonhosted.org/packages/09/13/610110fc5921d380516d03c26b652555f08aa0d23ea78a771231873c3638/regex-2026.7.19-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4c3501bfa814ab07b5580741f9bf78dfdfe146a04057f82df9e2402d2a975939", size = 784426, upload-time = "2026-07-19T00:17:52.454Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f5/1ef9e2a83a5947c57ebff0b377cb5727c3d5ec1992317a320d035cd0dbb6/regex-2026.7.19-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c4585c3e64b4f9e583b4d2683f18f5d5d872b3d71dcf24594b74ecc23602fa96", size = 860600, upload-time = "2026-07-19T00:17:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/a0/02/073af33a3ec149241d11c80acea91e722aa0adbf05addd50f251c4fe89c3/regex-2026.7.19-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:571fde9741eb0ccde23dd4e0c1d50fbae910e901fa7e629faf39b2dda740d220", size = 765950, upload-time = "2026-07-19T00:17:56.041Z" }, + { url = "https://files.pythonhosted.org/packages/81/a9/d1e9f819dc394a568ef370cd56cf25394e957a2235f8370f23b576e5a475/regex-2026.7.19-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:15b364b9b98d6d2fe1a85034c23a3180ff913f46caddc3895f6fd65186255ccc", size = 851794, upload-time = "2026-07-19T00:17:57.897Z" }, + { url = "https://files.pythonhosted.org/packages/03/3a/8ae83eda7579feacdf984e71fb9e70635fb6f832eeddca58427ec4fca926/regex-2026.7.19-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffd8893ccc1c2fce6e0d6ca402d716fe1b29db70c7132609a05955e31b2aa8f2", size = 789845, upload-time = "2026-07-19T00:17:59.97Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/c195cbfe5a75fdec64d8f6554fd15237b837919d2c61bdc141d7c807b08b/regex-2026.7.19-cp313-cp313-win32.whl", hash = "sha256:f0fa4fa9c3632d708742baf2282f2055c11d888a790362670a403cbf48a2c404", size = 267135, upload-time = "2026-07-19T00:18:01.958Z" }, + { url = "https://files.pythonhosted.org/packages/b2/80/a11de8404b7272b70acb45c1c05987cce60b45d5693da2e176f0e390d564/regex-2026.7.19-cp313-cp313-win_amd64.whl", hash = "sha256:d51ffd3427640fa2da6ade574ceba932f210ad095f65fcc450a2b0a0d454868e", size = 277747, upload-time = "2026-07-19T00:18:04.121Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/0f5c8eff1b4f1f3d83276d365fccecf666afcc7d947420943bf394d07adb/regex-2026.7.19-cp313-cp313-win_arm64.whl", hash = "sha256:c670fe7be5b6020b76bc6e8d2196074657e1327595bca93a389e1a76ab130ad8", size = 277129, upload-time = "2026-07-19T00:18:05.821Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4c/44b74742052cedda40f9ae469532a037112f7311a36669a891fba8984bb0/regex-2026.7.19-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db47b561c9afd884baa1f96f797c9ca369872c4b65912bc691cfa99e68340af2", size = 501134, upload-time = "2026-07-19T00:18:07.567Z" }, + { url = "https://files.pythonhosted.org/packages/f0/45/bbd038b5e39ee5613a5a689290145b40058cc152c41de9cc23639d2b9734/regex-2026.7.19-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65dcd28d3eba2ab7c2fd906485cc301392b47cc2234790d27d4e4814e02cdfda", size = 299418, upload-time = "2026-07-19T00:18:09.38Z" }, + { url = "https://files.pythonhosted.org/packages/65/38/c5bde94b4cedfd5850d64c3f08222d8e1600e84f6ee71d9b44b4b8163f74/regex-2026.7.19-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f2e7f8e2ab6c2922be02c7ec45185aa5bd771e2e57b95455ee343a44d8130dff", size = 294486, upload-time = "2026-07-19T00:18:11.188Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6a/2f5e107cb26c960b781967178899daf2787a7ab151844ed3c01d6fc95474/regex-2026.7.19-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe31f28c94402043161876a258a9c6f757cb485905c7614ce8d6cd40e6b7bdc1", size = 811643, upload-time = "2026-07-19T00:18:12.975Z" }, + { url = "https://files.pythonhosted.org/packages/37/d4/a2f963406d7d73a62eed84ba05a258afb6cad1b21aa4517443ce40506b78/regex-2026.7.19-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8f6fa298bb4f7f58a33334406218ba74716e68feddf5e4e54cd5d8082705abf", size = 871081, upload-time = "2026-07-19T00:18:14.733Z" }, + { url = "https://files.pythonhosted.org/packages/45/a3/44be546340bedb15f13063f5e7fe16793ea4d9ea2e805d09bd174ac27724/regex-2026.7.19-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cc1b2440423a851fad781309dd87843868f4f66a6bcd1ddb9225cf4ec2c84732", size = 917372, upload-time = "2026-07-19T00:18:16.724Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f6/e0870b0fd2a40dba0074e4b76e514b21313d37946c9248453e34ec43923e/regex-2026.7.19-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ac59a0900474a52b7c04af8196affc22bd9842acb0950df12f7b813e983609a", size = 816089, upload-time = "2026-07-19T00:18:18.617Z" }, + { url = "https://files.pythonhosted.org/packages/ae/27/957e8e22690ad6634572b39b71f130a6105f4d0718bb16849eac00fff147/regex-2026.7.19-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4896db1f4ce0576765b8272aa922df324e0f5b9bb2c3d03044ff32a7234a9aba", size = 785206, upload-time = "2026-07-19T00:18:20.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/a4/186e410941e731037c01166069ab86da9f65e8f8110c18009ccf4bd623ee/regex-2026.7.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4e6883a021db30511d9fb8cfb0f222ce1f2c369f7d4d8b0448f449a93ba0bdfc", size = 800431, upload-time = "2026-07-19T00:18:22.716Z" }, + { url = "https://files.pythonhosted.org/packages/73/9f/e4e10e023d291d64a33e246610b724493bf1ce98e0e59c9b7c837e5acfb7/regex-2026.7.19-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:09523a592938aa9f587fb74467c63ff0cf88fc3df14c82ab0f0517dcf76aaa62", size = 864906, upload-time = "2026-07-19T00:18:24.772Z" }, + { url = "https://files.pythonhosted.org/packages/24/57/ccb20b6be5f1f52a053d1ba2a8f7a077edb9d918248b8490d7506c6832b3/regex-2026.7.19-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1ebac3474b8589fce2f9b225b650afd61448f7c73a5d0255a10cc6366471aed1", size = 773559, upload-time = "2026-07-19T00:18:27.008Z" }, + { url = "https://files.pythonhosted.org/packages/a3/82/f3b263cf8fad927dc102891da8502e718b7ff9d19af7a2a07c03865d7188/regex-2026.7.19-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4a0530bb1b8c1c985e7e2122e2b4d3aedd8a3c21c6bfddae6767c4405668b56e", size = 857739, upload-time = "2026-07-19T00:18:29.107Z" }, + { url = "https://files.pythonhosted.org/packages/47/2e/1687bd1b6c2aed5e672ccf845fc11557821fe7366d921b50889ea5ce57bf/regex-2026.7.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef7eeb108c47ce7bcc9513e51bcb1bf57e8f483d52fce68a8642e3527141ae0", size = 804522, upload-time = "2026-07-19T00:18:31.362Z" }, + { url = "https://files.pythonhosted.org/packages/76/7c/cc4e7655181b2d9235b704f2c5e19d8eff002bbc437bae59baee0e381aca/regex-2026.7.19-cp313-cp313t-win32.whl", hash = "sha256:64b6ca7391a1395c2638dd5c7456d67bea44fc6c5e8e92c5dc8aa6a8f23292b4", size = 269141, upload-time = "2026-07-19T00:18:33.479Z" }, + { url = "https://files.pythonhosted.org/packages/bb/14/961b4c7b05a2391c32dbc85e27773076671ef8f97f36cec70fe414734c02/regex-2026.7.19-cp313-cp313t-win_amd64.whl", hash = "sha256:f04b9f56b0e0614c0126be12c2c2d9f8850c1e57af302bd0a63bed379d4af974", size = 280036, upload-time = "2026-07-19T00:18:35.419Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/795644550d788ddbb6dc458c95895f8009978ea6d6ea76b005eb3f45e8c9/regex-2026.7.19-cp313-cp313t-win_arm64.whl", hash = "sha256:fcee38cd8e5089d6d4f048ba1233b3ad76e5954f545382180889112ff5cb712d", size = 279394, upload-time = "2026-07-19T00:18:37.454Z" }, + { url = "https://files.pythonhosted.org/packages/d2/25/0c4c452f8ef3efe456745b2f33195f5904b573fb4c2ff3f0cb9ec188461e/regex-2026.7.19-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a81758ed242b861b72e778ba34d41366441a2e10b16b472784c88da2dea7e2dd", size = 496750, upload-time = "2026-07-19T00:18:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/24/9e/b70ca6c1704f6c7cd32a9e143c86cc5968d10981eca284bad670c245ea7d/regex-2026.7.19-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4aa5435cdb3eb6f55fe98a171b05e3fbcd95fadaa4aa32acf62afd9b0cfdbcac", size = 297093, upload-time = "2026-07-19T00:18:41.583Z" }, + { url = "https://files.pythonhosted.org/packages/87/74/0b692da2520d51fbff19c88b83d97e4c702909dd02386c585998b7e2dbed/regex-2026.7.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:60be8693a1dadc210bbcbc0db3e26da5f7d01d1d5a3da594e99b4fa42df404f5", size = 292043, upload-time = "2026-07-19T00:18:43.347Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a7/1d478e614016045a33feae57446215f9fd65b665a5ceb2f891fb3183bc52/regex-2026.7.19-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19662dbedbe783d323196312d38f5ba53cf56296378252171985da6899887d3", size = 797214, upload-time = "2026-07-19T00:18:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ae/11b9c9411d92c30e3d2db32df5a31133e4a99a8fc397a604fd08f6c4bffb/regex-2026.7.19-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d15df07081d91b76ff20d43f94592ee110330152d617b730fdbe5ef9fb680053", size = 866433, upload-time = "2026-07-19T00:18:47.315Z" }, + { url = "https://files.pythonhosted.org/packages/b1/62/2b2efc4992f91d6d204b24c647c9f9412e85379d92b7c0ab9fdae622327e/regex-2026.7.19-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:56ad4d9f77df871a99e25c37091052a02528ec0eb059de928ee33956b854b45b", size = 911360, upload-time = "2026-07-19T00:18:49.588Z" }, + { url = "https://files.pythonhosted.org/packages/14/71/986ceea9aa3da548bf1357cad89b63915ec6d21ec957c8113b29ece567df/regex-2026.7.19-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7322ec6cc9fba9d49ab888bb82d67ac5625627aa168f0165139b17018df3fb8a", size = 801275, upload-time = "2026-07-19T00:18:51.767Z" }, + { url = "https://files.pythonhosted.org/packages/15/be/ce9d9534b2cda96eab32c548261224b9b4e220a4126f098f60f42ae7b4cd/regex-2026.7.19-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c7472192ebfad53a6be7c4a8bfb2d64b81c0e93a1fc8c57e1dd0b638297b5d1", size = 777131, upload-time = "2026-07-19T00:18:54.053Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/58b5c710f2c3929515a25f3a1ca0dad0dcd4518d4fff3cf23bc7adb8dcd2/regex-2026.7.19-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c10b82c2634df08dfb13b1f04e38fe310d086ee092f4f69c0c8da234251e556e", size = 785020, upload-time = "2026-07-19T00:18:56.579Z" }, + { url = "https://files.pythonhosted.org/packages/84/03/5fe091935b74f15fe0f97998c215cae418d1c0413f6258c7d4d2e83aa37f/regex-2026.7.19-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:17ed5692f6acc4183e98331101a5f9e4f64d72fe58b753da4d444a2c77d05b12", size = 861263, upload-time = "2026-07-19T00:18:58.64Z" }, + { url = "https://files.pythonhosted.org/packages/d8/fa/d60bf82e10841eef62a9e32aac401468f05fddfbcb2942e342b1ba3d2433/regex-2026.7.19-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:22a992de9a0d91bda927bf02b94351d737a0302905432c88a53de7c4b9ce62e2", size = 766199, upload-time = "2026-07-19T00:19:00.705Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5d/11e64d151b0662b81d6bf644c74dc118d461df85bdf2577fadbbf751788a/regex-2026.7.19-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:618a0aed532be87294c4477b0481f3aa0f1520f4014a4374dd4cf789b4cd2c97", size = 851317, upload-time = "2026-07-19T00:19:03.015Z" }, + { url = "https://files.pythonhosted.org/packages/7c/34/532efb87488d90807bae6a443d357ee5e2728a478c597619c8aaa17cc0bd/regex-2026.7.19-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ce9e679f776649746729b6c86382da519ef649c8e34cc41df0d2e5e0f6c36d4", size = 789557, upload-time = "2026-07-19T00:19:05.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/90/3a8d5ca977171ec3ae21a71207d2228b2663bde14d7f7ef0e6363ecf9290/regex-2026.7.19-cp314-cp314-win32.whl", hash = "sha256:73f272fba87b8ccfe70a137d02a54af386f6d27aa509fbffdd978f5947aae1aa", size = 272531, upload-time = "2026-07-19T00:19:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/96/e1/8862885e70409de70e8c005f57fb2e7be8d9ef0317250d60f4c9660a300d/regex-2026.7.19-cp314-cp314-win_amd64.whl", hash = "sha256:d721e53758b2cca74990185eb0671dd466d7a388a1a45d0c6f4c13cef41a68ac", size = 280831, upload-time = "2026-07-19T00:19:09.46Z" }, + { url = "https://files.pythonhosted.org/packages/08/82/2693e53e29f9104d9de95d37ce4dd826bd32d5f9c0085d3aa6ac042675c4/regex-2026.7.19-cp314-cp314-win_arm64.whl", hash = "sha256:65fa6cb38ed5e9c3637e68e544f598b39c3b86b808ed0627a67b68320384b459", size = 281099, upload-time = "2026-07-19T00:19:11.398Z" }, + { url = "https://files.pythonhosted.org/packages/92/b7/9a01aa16461a18cde9d7b9c3ab21e501db2ce33725f53014342b91df2b0a/regex-2026.7.19-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:5a2721c8720e2cb3c209925dfb9200199b4b07361c9e01d321719404b21458b3", size = 501121, upload-time = "2026-07-19T00:19:13.425Z" }, + { url = "https://files.pythonhosted.org/packages/f3/5e/bbaeca815dc9191c424c94a4fdc5c87c75748a64a6271821212ebdd4e1a3/regex-2026.7.19-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:199535629f25caf89698039af3d1ad5fcae7f933e2112c73f1cdf49165c99518", size = 299415, upload-time = "2026-07-19T00:19:15.43Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d6/0dd1a321afaab95eb7ff44aa0f637301786f1dc71c6b797b9ed236ed8890/regex-2026.7.19-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9b60d7814174f059e5de4ab98271cc5ba9259cfea55273a81544dceea32dc8d9", size = 294483, upload-time = "2026-07-19T00:19:17.879Z" }, + { url = "https://files.pythonhosted.org/packages/92/5f/40bacf91d0904f812e13bbbab3864604c463eced8afdc54aeaa50492ea95/regex-2026.7.19-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbece16025afda5e3031af0c4059207e61dcf73ef13af844964f57f387d1c435", size = 811833, upload-time = "2026-07-19T00:19:20.102Z" }, + { url = "https://files.pythonhosted.org/packages/94/7c/4902744261f775aeede8b5627314b38482da29cf49a57b66a6fb753246c5/regex-2026.7.19-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d24ecb4f5e009ea0bd275ee37ad9953b32005e2e5e60f8bbae16da0dbbf0d3a0", size = 871270, upload-time = "2026-07-19T00:19:22.365Z" }, + { url = "https://files.pythonhosted.org/packages/16/70/6980c9be6bf21c0a60ed3e0aea39cf419ecf3b08d1d9947bc56e196ef186/regex-2026.7.19-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8cae6fd77a5b72dae505084b1a2ee0360139faf72fedbab667cd7cc65aae7a6a", size = 917534, upload-time = "2026-07-19T00:19:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/52/92/8b2bd872782ce8c42691e39acb38eb8efe014e5ddb78ad7d943d6f197ce9/regex-2026.7.19-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9724e6cb5e478cd7d8cabf027826178739cb18cf0e117d0e32814d479fa02276", size = 816135, upload-time = "2026-07-19T00:19:26.919Z" }, + { url = "https://files.pythonhosted.org/packages/de/2d/33a602f657bdc4041f17d79f92ab18261d255d91a06117a6e29df023e5e2/regex-2026.7.19-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:572fc57b0009c735ee56c175ea021b637a15551a312f56734277f923d6fd0f6c", size = 785492, upload-time = "2026-07-19T00:19:29.192Z" }, + { url = "https://files.pythonhosted.org/packages/9e/36/0987cf4cb271680064a70d24a475873775a151d0b7058698a006cb0cae4a/regex-2026.7.19-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:20568e182eb82d39a6bf7cff3fd58566f14c75c6f74b2c8c96537eecf9010e3a", size = 800658, upload-time = "2026-07-19T00:19:31.392Z" }, + { url = "https://files.pythonhosted.org/packages/a8/24/c14f31c135e1ba55fa4f9a58ca98d0842512bf6188230763c31c8f449e3b/regex-2026.7.19-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1d58561843f0ff7dc78b4c28b5e2dc388f3eff94ebc8a232a3adba961fc00009", size = 865073, upload-time = "2026-07-19T00:19:33.485Z" }, + { url = "https://files.pythonhosted.org/packages/14/85/181a12211f22469f24d2de1ebddfe397d2396e2c29013b9a58134a91069a/regex-2026.7.19-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:61bb1bd45520aacd56dd80943bd34991fb5350afdd1f36f2282230fd5154a218", size = 773684, upload-time = "2026-07-19T00:19:35.599Z" }, + { url = "https://files.pythonhosted.org/packages/23/58/bd1a0c1a62251366f8d21f41b1ea3c76994962071b8b6ea42f72d505c0f0/regex-2026.7.19-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:cd3584591ea4429026cdb931b054342c2bcf189b44ff367f8d5c15bc092a2966", size = 857769, upload-time = "2026-07-19T00:19:37.738Z" }, + { url = "https://files.pythonhosted.org/packages/e4/4f/f7e2dad6756b2fe1fe75dd90a628c3b45f249d39f948dd90cd2476325417/regex-2026.7.19-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cc26a66e212fa5d6c6170c3a40d99d888db3020c6fdab1523250d4341382e44", size = 804546, upload-time = "2026-07-19T00:19:40.229Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d7/01d31d5bdb09bc026fab77f59a371fdf8f9b292e4810546c56182ca70498/regex-2026.7.19-cp314-cp314t-win32.whl", hash = "sha256:2c4e61e2e1be56f63ec3cc618aa9e0de81ef6f43d177205451840022e24f5b78", size = 274526, upload-time = "2026-07-19T00:19:42.398Z" }, + { url = "https://files.pythonhosted.org/packages/52/0e/cea4ce73bc0a8247a0748228ae6669984c7e1f8134b6fa66e59c0572e0ea/regex-2026.7.19-cp314-cp314t-win_amd64.whl", hash = "sha256:c639ea314df70a7b2811e8020448c75af8c9445f5a60f8a4ced81c306a9380c2", size = 283763, upload-time = "2026-07-19T00:19:44.644Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b6/26e41975febae63b7a6e3e02f32cff6cff2e4f10d19c929082f56aebf7c6/regex-2026.7.19-cp314-cp314t-win_arm64.whl", hash = "sha256:9a15e785f244f3e07847b984ce8773fc3da10a9f3c131cc49a4c5b4d672b4547", size = 283451, upload-time = "2026-07-19T00:19:46.639Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/25/7113f6d5498888c5fb7db34081cba7d5971c4cb1bfb26819966eee68f003/ruff-0.16.1.tar.gz", hash = "sha256:fedad7c801dabd3fb9741d76aca39246e6ddd9ca446a015875207bf19f1e6bc7", size = 4877500, upload-time = "2026-07-30T19:37:01.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/bd/694da69368e0973de65df2ddc73ab18d43c469d5963d9b150911de6bc513/ruff-0.16.1-py3-none-linux_armv6l.whl", hash = "sha256:58edb313b88f0c5460a26adf5f39a37a3be789494a15e3e411e35fa78b89f9a0", size = 10839126, upload-time = "2026-07-30T19:36:13.697Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f0/b626e5d5bd0dd9576263658ef12885e2288afd1029a48e26ffed65ec1ac1/ruff-0.16.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fde5a99e2f97479af66edd6622c6d5a2a7592c77cf4153d9e4428f5eeb55b60c", size = 11070253, upload-time = "2026-07-30T19:36:17.14Z" }, + { url = "https://files.pythonhosted.org/packages/83/63/f40acfb6b35b88623e71684942b552c3edd96035f5d98f313815f7b277de/ruff-0.16.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0d4c20532fca4f7fa609369161d968dd28f65d83dabbd61d8e9c7edbf7001f6", size = 10561425, upload-time = "2026-07-30T19:36:20.04Z" }, + { url = "https://files.pythonhosted.org/packages/aa/dd/14ec0e9c2b4d315547dd38765004b4863e354e1b52cb308272215d9f6f6d/ruff-0.16.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30affbcedf59ad5703d9c91f82266e02b47739f797e1a7b6e158e5526a6dae38", size = 10948879, upload-time = "2026-07-30T19:36:22.476Z" }, + { url = "https://files.pythonhosted.org/packages/33/e9/9d870cbae575030fdef595f04b4b97573c525b5497cce4f4498cf2f85446/ruff-0.16.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24e9c631573cbca9d20f1283f8f479b2afa4a8503504822bd71a293889f16743", size = 10643691, upload-time = "2026-07-30T19:36:24.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/09/12743d544e2173f53ecd27217c65f90d2bc0f8424a66a60339e56bbc0457/ruff-0.16.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41bdd48fb420987a9b5212e4957c26ad4abce401fa9ea9d4d85843727945f4f", size = 11435354, upload-time = "2026-07-30T19:36:28.447Z" }, + { url = "https://files.pythonhosted.org/packages/7f/89/a1652b2daee52083c9554a6333b678a8b01d0400f976827bb87857f9449a/ruff-0.16.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0d1e1393b7648079e13669de1c1f4fde06d4583e84d8fd5c1551e0a77a2aa75", size = 12259033, upload-time = "2026-07-30T19:36:31.326Z" }, + { url = "https://files.pythonhosted.org/packages/16/96/ecdcb8c54ee7b123b487f807eb014e6e019155a0b81dfb669acd52f28ce3/ruff-0.16.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07bf434b1c95f4e093be4532068ef4fcf00924eb2ade8796075980902d6fd54a", size = 11667981, upload-time = "2026-07-30T19:36:34.394Z" }, + { url = "https://files.pythonhosted.org/packages/cd/90/c52e12e0d862e9572f2a33aa227409143520abe53111e9a6babbac7b4af8/ruff-0.16.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39897739f112253ee4fdd2e8aa9a4f9ded99fb2be367d5f31dfa4ded6025584c", size = 11468183, upload-time = "2026-07-30T19:36:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6b/4ffb7ad1d83eb16cf8cbb3c8815d3f11c88460fd162d4b372a2059be1c2a/ruff-0.16.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82ae3c0c0d74daf17b968a10b7b3bb3ef297ab7de0c1f749646b25e690ccb150", size = 11470071, upload-time = "2026-07-30T19:36:39.91Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/32ae7db4c0b5e32ab611787caa19d1546800676d79f7483b7100a3561bf4/ruff-0.16.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4d5f2ed10f8242d83fc08d521301089364e3375375705356f20c0e31606ef3ef", size = 10919503, upload-time = "2026-07-30T19:36:42.65Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ca/3d901ba6ad6fc38da39c3448fc6c59ac945679293a17c3ceb6d6c1cba13e/ruff-0.16.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a4665b309891f83f3e3c25447935f1213e9abbd4b5640af7a1f2def9f8d413c1", size = 10649861, upload-time = "2026-07-30T19:36:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/92/79/894ef1ced26552d5f8c9cf6d85b0687840e1128c55aeab7b9c2d54a0d880/ruff-0.16.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26e9ca5c9bc3971f20d3cf18a957f52ffd6a5f6564ff15c4912a144dcac22494", size = 11148137, upload-time = "2026-07-30T19:36:47.936Z" }, + { url = "https://files.pythonhosted.org/packages/2d/69/3609a09fa1cb46cc28b762363e440a354204e5dff01bd0c8d7437874d6b9/ruff-0.16.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:67e1e1e3fa4f0c82f0e36d4cd61e661f6e7a6196cb1aa92fe0828fa7b8f257cd", size = 11559211, upload-time = "2026-07-30T19:36:50.448Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8a/fb22af2fd78a736e241fabf67e30ce1799a64244026377a49e133af90762/ruff-0.16.1-py3-none-win32.whl", hash = "sha256:d31765e131295b8445caf301e3e8a85b34d1b9b211b4109b7ba457888b051806", size = 10838258, upload-time = "2026-07-30T19:36:53.298Z" }, + { url = "https://files.pythonhosted.org/packages/d4/35/e57fd9fb5d423961df087a00b12d42c0a830288dc2f3b45ecca299158b4f/ruff-0.16.1-py3-none-win_amd64.whl", hash = "sha256:09b05e8b90c2cb06ad63464350e7a45e8e44a2dfe52072ebfba6666ca8d3f596", size = 11961111, upload-time = "2026-07-30T19:36:56.107Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/240ea004bf6dc4feb40e9832f2205a476a47dd5b8a3f8211a5fc5f95e20e/ruff-0.16.1-py3-none-win_arm64.whl", hash = "sha256:dbaadaac38c70239f056d306b7476f246b0bf000fa6b3876402acbf5b227eaf8", size = 11309414, upload-time = "2026-07-30T19:36:58.79Z" }, +] + +[[package]] +name = "safetensors" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd23c8ac6141af5106e7ad5fedec4a3a6e3d60f94b7001/safetensors-0.8.0.tar.gz", hash = "sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d", size = 325846, upload-time = "2026-06-09T07:52:25.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/a0/f718cda65b05407d228f97602cf60dca269c979867aa5beb25410de26cd3/safetensors-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0", size = 473568, upload-time = "2026-06-09T07:52:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b1/fa7c600e7dceae12e9606c7578cbc9ff1e1ed55844883ee5c92205e86226/safetensors-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25", size = 484562, upload-time = "2026-06-09T07:52:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/65a7de0af421317bb36a067241e4235fff194eed60b961ed6d3f59a3fc60/safetensors-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235", size = 502844, upload-time = "2026-06-09T07:52:07.624Z" }, + { url = "https://files.pythonhosted.org/packages/91/4f/3175c9d75634e0e0dda0082794193521035edd7c70a6f212bf33ca06ddf4/safetensors-0.8.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0", size = 511823, upload-time = "2026-06-09T07:52:09.565Z" }, + { url = "https://files.pythonhosted.org/packages/20/87/846c289e7aa2299eff406335717cf43ce8777194ece8aad75772e0411615/safetensors-0.8.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98", size = 633461, upload-time = "2026-06-09T07:52:11.128Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/8d64d9df2c45d5ded401df889d0ad90882804ca172d79ec4f0df8f727fe0/safetensors-0.8.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358", size = 545148, upload-time = "2026-06-09T07:52:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/f203ff3a3ddfe19308efc83c5a3a29ed02bf786732ec35e68bf9162f3365/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774", size = 516040, upload-time = "2026-06-09T07:52:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/46/fb/cdaed17ceb2948784fd9c36b6fd3e951b608547cea81a48e8ee6f8cfdfcb/safetensors-0.8.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78", size = 513832, upload-time = "2026-06-09T07:52:12.37Z" }, + { url = "https://files.pythonhosted.org/packages/0d/49/1e15de264dcc3b77943d2d0c56a95809956883b1c2d6d585c792523f180b/safetensors-0.8.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca", size = 559930, upload-time = "2026-06-09T07:52:14.743Z" }, + { url = "https://files.pythonhosted.org/packages/2a/43/bf38443278eab4b1be1fce2931e2b012ad9cb7df52ada751d0aab8f7659a/safetensors-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4", size = 678670, upload-time = "2026-06-09T07:52:20.032Z" }, + { url = "https://files.pythonhosted.org/packages/72/e3/68cd3fa5b48488e84add63e04cb12f3bc28ae4638c06d4508c6e88823d0e/safetensors-0.8.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc", size = 786679, upload-time = "2026-06-09T07:52:21.322Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/1c19c509d56e01f4fbb3d0a2e597450f6cc04d1d56cf52defb0a62dfd715/safetensors-0.8.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d", size = 765683, upload-time = "2026-06-09T07:52:22.594Z" }, + { url = "https://files.pythonhosted.org/packages/27/43/41c1621732edd934d868a00d1b891584c892a7b62a9aab82ea5a0a5623ee/safetensors-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846", size = 722361, upload-time = "2026-06-09T07:52:23.924Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3f/73ccf82579412b4a71c4ca673f10b5f1f888d7cf5af7fe24f27d30307be4/safetensors-0.8.0-cp310-abi3-win32.whl", hash = "sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d", size = 342401, upload-time = "2026-06-09T07:52:28.895Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6d/3fba214c1e5e0f69991677ec3bc17023f0421776975e1de0c682dca475e2/safetensors-0.8.0-cp310-abi3-win_amd64.whl", hash = "sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f", size = 355540, upload-time = "2026-06-09T07:52:27.832Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fc/7eedc3510d97878876e32774eebbeb61c43f148a96e915c84229a3e967aa/safetensors-0.8.0-cp310-abi3-win_arm64.whl", hash = "sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452", size = 340500, upload-time = "2026-06-09T07:52:26.745Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'linux'", +] +dependencies = [ + { name = "joblib", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/3e/daed796fd69cce768b8788401cc464ea90b306fb196ae1ffed0b98182859/scikit_learn-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f", size = 9336221, upload-time = "2025-09-09T08:20:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ce/af9d99533b24c55ff4e18d9b7b4d9919bbc6cd8f22fe7a7be01519a347d5/scikit_learn-1.7.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c", size = 8653834, upload-time = "2025-09-09T08:20:22.073Z" }, + { url = "https://files.pythonhosted.org/packages/58/0e/8c2a03d518fb6bd0b6b0d4b114c63d5f1db01ff0f9925d8eb10960d01c01/scikit_learn-1.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8", size = 9660938, upload-time = "2025-09-09T08:20:24.327Z" }, + { url = "https://files.pythonhosted.org/packages/2b/75/4311605069b5d220e7cf5adabb38535bd96f0079313cdbb04b291479b22a/scikit_learn-1.7.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18", size = 9477818, upload-time = "2025-09-09T08:20:26.845Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9b/87961813c34adbca21a6b3f6b2bea344c43b30217a6d24cc437c6147f3e8/scikit_learn-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5", size = 8886969, upload-time = "2025-09-09T08:20:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" }, + { url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" }, + { url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" }, + { url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" }, + { url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" }, + { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" }, + { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/a3038cb0293037fd335f77f31fe053b89c72f17b1c8908c576c29d953e84/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7", size = 9212382, upload-time = "2025-09-09T08:20:54.731Z" }, + { url = "https://files.pythonhosted.org/packages/40/dd/9a88879b0c1104259136146e4742026b52df8540c39fec21a6383f8292c7/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe", size = 8592042, upload-time = "2025-09-09T08:20:57.313Z" }, + { url = "https://files.pythonhosted.org/packages/46/af/c5e286471b7d10871b811b72ae794ac5fe2989c0a2df07f0ec723030f5f5/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f", size = 9434180, upload-time = "2025-09-09T08:20:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fd/df59faa53312d585023b2da27e866524ffb8faf87a68516c23896c718320/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0", size = 9283660, upload-time = "2025-09-09T08:21:01.71Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057, upload-time = "2025-09-09T08:21:04.234Z" }, + { url = "https://files.pythonhosted.org/packages/55/87/ef5eb1f267084532c8e4aef98a28b6ffe7425acbfd64b5e2f2e066bc29b3/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8", size = 9558731, upload-time = "2025-09-09T08:21:06.381Z" }, + { url = "https://files.pythonhosted.org/packages/93/f8/6c1e3fc14b10118068d7938878a9f3f4e6d7b74a8ddb1e5bed65159ccda8/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a", size = 9038852, upload-time = "2025-09-09T08:21:08.628Z" }, + { url = "https://files.pythonhosted.org/packages/83/87/066cafc896ee540c34becf95d30375fe5cbe93c3b75a0ee9aa852cd60021/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c", size = 9527094, upload-time = "2025-09-09T08:21:11.486Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2b/4903e1ccafa1f6453b1ab78413938c8800633988c838aa0be386cbb33072/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c", size = 9367436, upload-time = "2025-09-09T08:21:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749, upload-time = "2025-09-09T08:21:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/dee5acf66837852e8e68df6d8d3a6cb22d3df997b733b032f513d95205b7/scikit_learn-1.7.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33", size = 9208906, upload-time = "2025-09-09T08:21:18.557Z" }, + { url = "https://files.pythonhosted.org/packages/3c/30/9029e54e17b87cb7d50d51a5926429c683d5b4c1732f0507a6c3bed9bf65/scikit_learn-1.7.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615", size = 8627836, upload-time = "2025-09-09T08:21:20.695Z" }, + { url = "https://files.pythonhosted.org/packages/60/18/4a52c635c71b536879f4b971c2cedf32c35ee78f48367885ed8025d1f7ee/scikit_learn-1.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106", size = 9426236, upload-time = "2025-09-09T08:21:22.645Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/290362f6ab582128c53445458a5befd471ed1ea37953d5bcf80604619250/scikit_learn-1.7.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61", size = 9312593, upload-time = "2025-09-09T08:21:24.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/87/24f541b6d62b1794939ae6422f8023703bbf6900378b2b34e0b4384dfefd/scikit_learn-1.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8", size = 8820007, upload-time = "2025-09-09T08:21:26.713Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'linux'", +] +dependencies = [ + { name = "joblib", marker = "python_full_version >= '3.11'" }, + { name = "narwhals", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/be/e844fd9586e66540a15b71924d17a6cbc1bb749e81ddd0a796bcdba4c055/scikit_learn-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9db6f4d34e68c8899e4cab27fdf8eafe6ed21f2ba52ceb25ea250cd237f8e47b", size = 8789686, upload-time = "2026-06-02T11:53:05.439Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/ff880f62677a17d035817d543cb0fc8727d01eccbee81c5f7fc733a9d856/scikit_learn-1.9.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f401448645a3e7bc115aa3c094097865155b34bff1cba8101857d9104e99074c", size = 8256782, upload-time = "2026-06-02T11:53:08.904Z" }, + { url = "https://files.pythonhosted.org/packages/25/64/eb40435e1a508ab1b4e284ce43ae80f6a162e5be5e38ed5a6fab467a9ea4/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd3a8ef0c758555a3b23c03adaa858af32f7736785ded50ad5991f59c4ed03fa", size = 8992419, upload-time = "2026-06-02T11:53:11.551Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/4810a28e473185429e45a57eebcc91fc991b33d889cc0676063e671db03d/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7e254636164090da847715a27f8e5478feb98c40a9e0ee90cbd277de9e5ceb8", size = 9281411, upload-time = "2026-06-02T11:53:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/be3d369f40d8178ba3bd86635d132e08cb5329b023e4669d9426d84bc007/scikit_learn-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:5dc1818c77575d149e25fce9ef82dd7b7263ae372f03494158668ad632a69759", size = 8272736, upload-time = "2026-06-02T11:53:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/37/79/a733f02dc2118da7e77a134b34f39f40201a353311b011d20859d2db3556/scikit_learn-1.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:366652351f092b219c248f1e72821e841960a63d8f358f1dcfd54dc1cbdbbc28", size = 7919564, upload-time = "2026-06-02T11:53:21.2Z" }, + { url = "https://files.pythonhosted.org/packages/ac/20/75f915ff375d6249e6550ac740fdbbd66159a068fd3af1400ff62036b07a/scikit_learn-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2bd41b0d201bc81575531b96b713d3eb5e5f50fb0b82101ff0f92294fdc236ac", size = 8741122, upload-time = "2026-06-02T11:53:24.08Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d5/2b5148f2279196775e1db2aeb85d14b70ac80e7e32b3b28e7ebeafb0901d/scikit_learn-1.9.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5be45aa4a42a68a533913a6ed736cf309de2226411c79ef8d609a5456f1939b1", size = 8261512, upload-time = "2026-06-02T11:53:27.183Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ee/5adbc77656b71f9456a2f5a7a9fdb4bcf9207a6b962889f1c2f9323afa4e/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e50ed4da51974e86e940690e9a3d82e729b62b5a49f7c9bac534d515d39d86f", size = 8837603, upload-time = "2026-06-02T11:53:30.328Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c2/63fdda36c56437eeb44aaf9493c8bcd62ce230ab1598924fc626ffbfa943/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:056c92bb67ad4c28463c2f2653d9701449201e7e7a9e94e321be0f71c4fef2b8", size = 9132097, upload-time = "2026-06-02T11:53:33.456Z" }, + { url = "https://files.pythonhosted.org/packages/83/a4/c8e67227c680e2259c8864ae72ff48b06e16a6f51253a22167aa02a8aa4e/scikit_learn-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4306775fad04cc4b472a1b15af1ae9cede1540fbfcc17fbce3767cd8dc7ae283", size = 8211173, upload-time = "2026-06-02T11:53:36.602Z" }, + { url = "https://files.pythonhosted.org/packages/cf/fd/3c0863792e98e67e9184aa4029288a175935eb65443afcd30d4f143450cf/scikit_learn-1.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:26e22435f63bcdcf396b574273f29f13dd531f5ea035801f5be10ba1540a4e60", size = 7867451, upload-time = "2026-06-02T11:53:39.075Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/cf3310626b6d48d3e9be69a1223f9180360b5e6edb045f50fade723ce494/scikit_learn-1.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:80746d63bd4b6eaca54d36fe5feaf4d28bb38dc6f9470f81c7cad7c40155f119", size = 8705188, upload-time = "2026-06-02T11:53:41.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/04/5acd7ae280c5f93b6ac5ef6cdec14eef4c8d1cd91d85b3292989c94d96b1/scikit_learn-1.9.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5b934c45c252844a91d69fda3a34cff5e7307e1db10d77cb10a3980312c74713", size = 8228299, upload-time = "2026-06-02T11:53:44.817Z" }, + { url = "https://files.pythonhosted.org/packages/0c/39/ffe829a5b8ecb40a518724a997794657fdc354ada5e8fe8e64d998c0bac9/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:38c3dcb9a1ffb85505ec53d54c7b4aea0cff70050425a7760c2af661ac85df05", size = 8789690, upload-time = "2026-06-02T11:53:47.461Z" }, + { url = "https://files.pythonhosted.org/packages/1f/88/8dab5de10c638c083772a6be83a3d8106ced492f74a928c8693638e5bb50/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da76d09304a4706db7cc1e3ebaa3b6b98a67365cc11d2996c4f1e58ba47df714", size = 9087723, upload-time = "2026-06-02T11:53:50.702Z" }, + { url = "https://files.pythonhosted.org/packages/20/3f/7917ca72464038f6240ec70c29f94862d08a34a74291ae4d4ec5eb8186a0/scikit_learn-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5808d98f15c6bf6d9d96d2348c1997392a5888ce7097e664105f930c4bca1277", size = 8184330, upload-time = "2026-06-02T11:53:53.396Z" }, + { url = "https://files.pythonhosted.org/packages/78/c7/15739eb2f61fda3c54639e9942414e5a19ad8a8d1f5a3266afad7cb7df80/scikit_learn-1.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:d77f54c017633791bc0225a43e2f8d03745fdcfe4880268fcc4df15f505dec2e", size = 7840653, upload-time = "2026-06-02T11:53:56.035Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/c9a35cf59b20a86fec24d306f1547b78dec194b08d367ce2a3e4854169d9/scikit_learn-1.9.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9656acd4e93f74e0b66c8a36c88830a99252dfa900044d36bc2212ae89a47162", size = 8713289, upload-time = "2026-06-02T11:53:58.788Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a7/552a7821597c632b907f7bfe8f36f9f572777af8ef8a48353041cf8e091a/scikit_learn-1.9.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:24360002ae845e7866522b0a5bbf690802e7bc388cac8663502e78aa98598aa2", size = 8245141, upload-time = "2026-06-02T11:54:01.694Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/f4a0c4fe9711154cddabf913471153af79056382ddc612cfe5ee0ff4b72e/scikit_learn-1.9.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5162ad10a418c8a282dde04c9aa06965de3e9a65f33c1440c0ae69bb1a09d913", size = 8847671, upload-time = "2026-06-02T11:54:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/f0/af/4d72d9e475ac83719160c662619e4bf7b95c19507cd582e7d0167a3c3dae/scikit_learn-1.9.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fea2cc5677ab49d6f5bade978c866da44957b712d92e9635e8b4f723013c3cb", size = 9118104, upload-time = "2026-06-02T11:54:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d5/6a58eea2cb9abbb9b3f2bb8b2cfb3243d1152d69f442d256c7af71304769/scikit_learn-1.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:64fa347efc1c839c487433e40c5144d38c336e8a2b59c81aa8660373945c2673", size = 8290674, upload-time = "2026-06-02T11:54:10.087Z" }, + { url = "https://files.pythonhosted.org/packages/65/5b/d4c879cf358f1187141cf90ced473f087183489090244f50c124a2ee478b/scikit_learn-1.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:1b944b6db288f6b926e3650026ddafb988929de95d11fc2cc5fa117773c9ba42", size = 7978807, upload-time = "2026-06-02T11:54:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/8a/43/bfae3121ec67ae09150d453c442c7c1cc166e9aefe056e6ab3b7728a5cfc/scikit_learn-1.9.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4ccacf04ca5f4b492158a5f28afe0ace43f81b2571e4b9a66d34848b46128949", size = 9031941, upload-time = "2026-06-02T11:54:15.436Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/20a4546eb17f3b25d3c66df15810411c14ed5065bcfab50b53c96fb627b2/scikit_learn-1.9.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ee1a8db2c18c08e34c7412d4b10be1cac214cd4ea7dc9715a6a327eb49a37c96", size = 8613528, upload-time = "2026-06-02T11:54:18.842Z" }, + { url = "https://files.pythonhosted.org/packages/18/3c/e440e039bb82cd19004edaaad00acbde0fb9b461083c3ecf37941c557312/scikit_learn-1.9.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:147e9329ef0e39f75d4cffa02b2aa48d827832684926cd5210d9a2cb5c57246b", size = 8855050, upload-time = "2026-06-02T11:54:21.699Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/b341b8dab5998da6270a3a42c2152c578501354d36f944b5856757035ef8/scikit_learn-1.9.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bad8f8b9950321b54c965fdcbac6c6c55e79e16646b49977bcf3668d3870a1a", size = 9097190, upload-time = "2026-06-02T11:54:24.454Z" }, + { url = "https://files.pythonhosted.org/packages/fb/de/b650b4d69b84468cfa2e28a3ff7b8103743029e6446ce1a97fe060ef688c/scikit_learn-1.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:78fc56eafd4edb9575d2d8950d1dd152061abb573341a1cb7e099fc40f6c6666", size = 8963204, upload-time = "2026-06-02T11:54:27.428Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/ff83d76d7418112e5a61326443cdda87be3545dd8d6599c95b2481a4419e/scikit_learn-1.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:051075bda8b7aab87b1906ab3d4740a1e1224a19d7b3781a576736edc94e76aa", size = 8222661, upload-time = "2026-06-02T11:54:30.192Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'linux'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'linux'", +] +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "scipy" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform != 'linux'", +] +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" }, + { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" }, + { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" }, + { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253, upload-time = "2026-06-19T15:00:43.216Z" }, + { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758, upload-time = "2026-06-19T15:00:46.663Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514, upload-time = "2026-06-19T15:00:49.399Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398, upload-time = "2026-06-19T15:00:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032, upload-time = "2026-06-19T15:00:54.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333, upload-time = "2026-06-19T15:00:58.152Z" }, + { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216, upload-time = "2026-06-19T15:01:01.155Z" }, + { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960, upload-time = "2026-06-19T15:01:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845, upload-time = "2026-06-19T15:01:07.822Z" }, + { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971, upload-time = "2026-06-19T15:01:11.076Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325, upload-time = "2026-06-19T15:01:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110, upload-time = "2026-06-19T15:01:16.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811, upload-time = "2026-06-19T15:01:20.488Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644, upload-time = "2026-06-19T15:01:23.364Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318, upload-time = "2026-06-19T15:01:26.002Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320, upload-time = "2026-06-19T15:01:28.881Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541, upload-time = "2026-06-19T15:01:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480, upload-time = "2026-06-19T15:01:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390, upload-time = "2026-06-19T15:01:38.091Z" }, + { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661, upload-time = "2026-06-19T15:01:40.817Z" }, +] + +[[package]] +name = "sentence-transformers" +version = "5.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "torch", version = "2.13.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, + { name = "torch", version = "2.13.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/80/573ab31b77bdfa8f18051188adff3405e928386287cd6f756eff5777dd82/sentence_transformers-5.6.1.tar.gz", hash = "sha256:16af5d682ef66672b076d58599a23905800e850ec2bfb1865938306bf684ad72", size = 452185, upload-time = "2026-07-23T14:40:41.589Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ad/8f73f512dc7ad4031d2b64cbb67f70bdfb355756afbe0db610a5146415c1/sentence_transformers-5.6.1-py3-none-any.whl", hash = "sha256:cefbb17b6325a982a4732c8c49fb013375392687049d1de3d435c4b04060680b", size = 596677, upload-time = "2026-07-23T14:40:40.312Z" }, +] + +[[package]] +name = "setuptools" +version = "83.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "starlette" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, + { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "torch" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'linux'", + "python_full_version < '3.11' and sys_platform != 'linux'", +] +dependencies = [ + { name = "filelock", marker = "sys_platform != 'linux'" }, + { name = "fsspec", marker = "sys_platform != 'linux'" }, + { name = "jinja2", marker = "sys_platform != 'linux'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform != 'linux'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and sys_platform != 'linux'" }, + { name = "setuptools", marker = "sys_platform != 'linux'" }, + { name = "sympy", marker = "sys_platform != 'linux'" }, + { name = "typing-extensions", marker = "sys_platform != 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/e7/19894fdb51c7dbaf94f5a79bb0871da0992e8e4241e579cb006da46d2e58/torch-2.13.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:94f0de129916f77b8dc2c7a8eff644cfeddfe59e39c9f55e9f6e17543410281d", size = 111178962, upload-time = "2026-07-08T16:05:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c9/0bb9d097b03cbaf96bb75b15e867347b8e41bfcdfe0539452d17d9e63993/torch-2.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:2bd30b6b730d987fa386ce3898933762c5cb8cc82eb0535211d787cc3ce2dfeb", size = 122015602, upload-time = "2026-07-08T16:05:45.25Z" }, + { url = "https://files.pythonhosted.org/packages/5b/fe/cba54dc58523434919b66f13a667e36e436deddd77ca519e96553617d4ec/torch-2.13.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:e76f9bcecc52b8ff711239a2f7547d5353df95878ab232f0773c1d95928b92f8", size = 111187938, upload-time = "2026-07-08T16:05:17.065Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fd/0f2ce40f58aefbdb3392f9acce3c8171940943ae2d661f70558bfa73befb/torch-2.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:a0d8b11f16a48d60e2015d8213aa0390744cbebb98e58b62b3514dddc656e330", size = 122015870, upload-time = "2026-07-08T16:05:27.59Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3a/ed0f4d4d1dcde03bced7aac9a28e800abcdc0cbd06b6775044c9fbd877b7/torch-2.13.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2fe228aba290d14b9f31b049be550dbd469c3fd3013d7a19705b30454da97027", size = 111213045, upload-time = "2026-07-08T16:05:22.997Z" }, + { url = "https://files.pythonhosted.org/packages/21/d6/e8f3c6f7e01f626f77259de9860d2a78bc84c40539e28e79b7e98b0bb659/torch-2.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:024c6cc0c1b085f2f91f20a3dc27b0471d021c31ce84b81be3afdc39f791fd9d", size = 122057313, upload-time = "2026-07-08T16:03:53.43Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fa/c1c10b7aff4a9a3e8956d4f0a5f468fa6db7abc3208805719076772b4833/torch-2.13.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:33449899ce5496c1b84b4853179d94fd102028ae1407314d9fb956bb79e70d09", size = 111213743, upload-time = "2026-07-08T16:03:28.579Z" }, + { url = "https://files.pythonhosted.org/packages/91/3d/e7adcc6aaf36961cd18f56cf8ad0f3058c3a5c84ccf391762176c94581b8/torch-2.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:49b58f1e2c52440abb6f17c28f0335fe6c6d01ad1a7f55b0183b81e4b34d64e6", size = 122057920, upload-time = "2026-07-08T16:03:01.808Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/6dcc7f0c07052102dd36f83cbc5800842a909c8c3fbf1a7f8a5844954de9/torch-2.13.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d849b390e07d8d333ce8ecaf91b273c656c598379a19c9acf1318a883f6b391c", size = 111227066, upload-time = "2026-07-08T16:03:33.6Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0c/7d1deb6bce5bc3e6042caf39100ac768eba3b9a098e1dddd16f75bd6489b/torch-2.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f8573e3ce9ebcd53fe922f01077a6085ccdfbe5f12fd215883a9d87d7a744fd", size = 122051871, upload-time = "2026-07-08T16:03:23.521Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ce/aa8b7f9949d32e0f2f624f342bc3b48112c1b8a130288465938bc83bcbf9/torch-2.13.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c28def70706c2f9ecc752574766e8ae4da9b810ab6676b611166761a78a9f1e1", size = 111537025, upload-time = "2026-07-08T16:02:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/56/94/655c91992a882bd5071aa0b5d22a07dbb130d801e872be97c0b627a7c693/torch-2.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a7de8a313090dc5c7d7ba4bfe5c3be222528f9a4dba1acc83bddb1157360c4b8", size = 122306773, upload-time = "2026-07-08T16:02:39.832Z" }, +] + +[[package]] +name = "torch" +version = "2.13.0+cpu" +source = { registry = "https://download.pytorch.org/whl/cpu" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", +] +dependencies = [ + { name = "filelock", marker = "sys_platform == 'linux'" }, + { name = "fsspec", marker = "sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'linux'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform == 'linux'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "sys_platform == 'linux'" }, + { name = "sympy", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp310-cp310-linux_s390x.whl", hash = "sha256:0555fde6108ca90247ae33d4e1237cbae475c86a223bb2f0f91d9addf1f611bd", upload-time = "2026-07-08T19:27:11Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:f028e428bddee95cdb86e2470254e95c9af629362488550c200ed4793125a817", upload-time = "2026-07-08T19:27:21Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:f5cbb61180a9793d9e12fe115a2310d2600bd449dfb9a01ec5640e21359fa5ea", upload-time = "2026-07-08T19:27:32Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp311-cp311-linux_s390x.whl", hash = "sha256:6e9817dbdf5ea76789babd46e457eac5bf14ff566cf85f8addbfdff2d56601ce", upload-time = "2026-07-08T19:27:52Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:84453b69508ec79902f899c5ed9495acb9e2bbe9fda5f1d5d6f19e3c3842e1a7", upload-time = "2026-07-08T19:28:03Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:6746dbcbeb526eb61330b76b41ff1b4eb848951103a892eeb080dfa2b264667b", upload-time = "2026-07-08T19:28:16Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:ffadde149901c8afa138daa38d898264003cfcf1a3336ca5cd964b5af227d867", upload-time = "2026-07-08T19:28:41Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6f307c2c32d764ffc6ff6893b801fad6d4752f3e67966cb8abf1843427c02604", upload-time = "2026-07-08T19:28:51Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ca4a9394b0c771238a4f73590fdbbc4debad85ed0fa63d026ae1b085da7d6e2", upload-time = "2026-07-08T19:29:03Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:966d020354f465672dc7dd10d3a5c6cd17d7eb48620aa1d265b48a1f78f06898", upload-time = "2026-07-08T19:29:30Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:0b8f7d0423027ae8b90c7977c627f3379f325363a08224dffad9b4b2d684a83d", upload-time = "2026-07-08T19:29:40Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3fbf9c9d1f3c10c2d59d04aca426dee9ccc6ceb32d255c61e93acc3b4f75fae6", upload-time = "2026-07-08T19:29:54Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314-linux_s390x.whl", hash = "sha256:dec241fef3984c0d1edadd1f58708e218d4eae881ceef7bc10cf9964d41b68b9", upload-time = "2026-07-08T19:30:20Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:ca021f9eb2f8345c83fa03e3a04587308afb8df71bd472670b3ece00df58621c", upload-time = "2026-07-08T19:30:32Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d20fa53ee744502fa4c69818a720b05ca0d37abd055d4f6e66cae155114bc691", upload-time = "2026-07-08T19:30:45Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314t-linux_s390x.whl", hash = "sha256:991cc14b39e751122c01f017be6448533989868731cb5eecd1006893d26787c2", upload-time = "2026-07-08T19:31:09Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:7b8d26e29bceafbdaa8d63bfe7612f23875b5af2cc07e13f809c3ed890bbe1d8", upload-time = "2026-07-08T19:31:21Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:b222c15a0fc2ce207d1c1a59700b46c8fa6748df1f447ad11e5c870dde0933d9", upload-time = "2026-07-08T19:31:35Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5002ca81af00ae69b57540f615b58b8ae922b6d4848176b366a52bd2196e6", upload-time = "2026-07-08T19:32:00Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:1a3a35229fdc13446b4eab50e7fcf9399ff941e89a3b761497786297a5d8dde5", upload-time = "2026-07-08T19:32:16Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:8e109528e6bab044815daebaf71770fbaace3a66ef1c816cb55c875350f78a60", upload-time = "2026-07-08T19:32:30Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:222a6681467cc7f6f05cd3068dfbc603def3a1e46d1d4620c1c8cdf6178bd563", upload-time = "2026-07-08T19:32:44Z" }, +] + +[[package]] +name = "tqdm" +version = "4.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" }, +] + +[[package]] +name = "transformers" +version = "5.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/fb/2a2ba88f325e68a921d8b69ff63b477830b2e73ade9a3c8c8cab2f06d741/transformers-5.14.1.tar.gz", hash = "sha256:60d196c27781eacf8637e2b533f517582907ad6f9ae142046d6b69431a5b2173", size = 9295927, upload-time = "2026-07-16T09:41:57.773Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/67/8d85ca2323233ae3c0365a659c4e52ee1f587b440e4bc577e7d8e4416d0f/transformers-5.14.1-py3-none-any.whl", hash = "sha256:9db974c4079ede2d1a3ea7ca5a240df33f2cc26fc2b36ba64c5f2a4f43b6e725", size = 11625234, upload-time = "2026-07-16T09:41:54.143Z" }, +] + +[[package]] +name = "typer" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/78/fda3361b56efc27944f24225f6ecd13d96d6fcfe37bd0eb34e2f4c63f9fc/typer-0.27.0.tar.gz", hash = "sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5", size = 203430, upload-time = "2026-07-15T19:21:07.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/03/26a383c9e58c213199d1aad1c3d353cfc22d4444ec6d2c0bf8ad02523843/typer-0.27.0-py3-none-any.whl", hash = "sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1", size = 122716, upload-time = "2026-07-15T19:21:05.553Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "umap-learn" +version = "0.5.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numba" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pynndescent" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/ee/af4171241117f85c74b5ca6448ea1033cc28d599c13651d67289bacd4083/umap_learn-0.5.12.tar.gz", hash = "sha256:6aff02ecac5f2aad9f3c65ee518d7ae93e1a985ae38721fdcffceee4232c33c7", size = 96672, upload-time = "2026-04-08T20:03:54.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/98/f63318ccbe75c810011fe9233884c5d348d94d90005de1b79e5f93bef9c0/umap_learn-0.5.12-py3-none-any.whl", hash = "sha256:f2a85d2a2adcb52b541bed9b27a23ca169b56bb1b23283abeebfb8dfb8a42fe5", size = 91849, upload-time = "2026-04-08T20:03:52.561Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/18/ccce41535dee1be77735592bd19965f3972c82e07ee703d324709496b716/uvicorn-0.52.1.tar.gz", hash = "sha256:112ec661814189acbccd3f7b86460147cc065fc92c0821afa78918780e4354dd", size = 100571, upload-time = "2026-08-01T18:19:30.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d5/68e6e9bca63c0badf67002890a46d3784c958de45b65e1275ec583ca1f06/uvicorn-0.52.1-py3-none-any.whl", hash = "sha256:e4403f9d93188cf9d1088e9f40e3acd12630e2df8675316704379a7fc20fff6a", size = 79859, upload-time = "2026-08-01T18:19:29.294Z" }, +] + +[[package]] +name = "vhectorlab-backend" +version = "2.3.0" +source = { editable = "." } +dependencies = [ + { name = "fastapi" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "orjson" }, + { name = "sentence-transformers" }, + { name = "torch", version = "2.13.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, + { name = "torch", version = "2.13.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" }, + { name = "umap-learn" }, + { name = "uvicorn" }, +] + +[package.optional-dependencies] +dev = [ + { name = "httpx" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.110.0" }, + { name = "httpx", specifier = ">=0.27.0" }, + { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27.0" }, + { name = "huggingface-hub", specifier = ">=0.21.0" }, + { name = "numpy", specifier = ">=1.26.0" }, + { name = "orjson", specifier = ">=3.11.9" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.3.0" }, + { name = "sentence-transformers", specifier = ">=2.5.0" }, + { name = "torch", marker = "sys_platform != 'linux'", specifier = ">=2.2.0" }, + { name = "torch", marker = "sys_platform == 'linux'", specifier = ">=2.2.0", index = "https://download.pytorch.org/whl/cpu" }, + { name = "umap-learn", specifier = "==0.5.12" }, + { name = "uvicorn", specifier = ">=0.28.0" }, +] +provides-extras = ["dev"] diff --git a/backend/vocab_embeddings.py b/backend/vocab_embeddings.py new file mode 100644 index 0000000000000000000000000000000000000000..61fd6f99e0c9a9a96d69ba42ec79b80575a22dc2 --- /dev/null +++ b/backend/vocab_embeddings.py @@ -0,0 +1,157 @@ +""" +Vocab embeddings NPZ cache — load/save metadata + selection compatibility. + +Strategy (multillm Slice 3 / §3.3): on model_name or dim mismatch, AppState +logs a loud warning and re-encodes from the vocab text file (auto-rebuild), +then overwrites the NPZ when a path is available. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import numpy as np +from backend.model_catalog import ModelSelection + + +@dataclass(frozen=True, slots=True) +class VocabEmbeddingsCache: + words: list[str] + embeddings: np.ndarray + model_name: str | None = None + truncate_dim: int | None = None + embedding_dim: int | None = None + + +def _scalar_str(raw: Any) -> str | None: + if raw is None: + return None + if hasattr(raw, "item"): + try: + return str(raw.item()) + except (ValueError, TypeError): + pass + text = str(raw).strip() + return text or None + + +def _scalar_int(raw: Any) -> int | None: + if raw is None: + return None + if hasattr(raw, "item"): + try: + raw = raw.item() + except (ValueError, TypeError): + pass + if raw is None or raw == "": + return None + return int(raw) + + +def load_vocab_embeddings_npz(npz_path: Path) -> VocabEmbeddingsCache: + """Load precomputed vocab embeddings NPZ (words, embeddings, optional metadata).""" + data = np.load(npz_path, allow_pickle=True) + if "words" not in data or "embeddings" not in data: + raise ValueError(f"Invalid vocab embeddings file (missing keys): {npz_path}") + words = [str(w).strip().lower() for w in data["words"].tolist()] + embeddings = np.asarray(data["embeddings"], dtype=np.float32) + if embeddings.ndim != 2 or len(words) != embeddings.shape[0]: + raise ValueError( + f"Vocab NPZ shape mismatch: words={len(words)} embeddings={embeddings.shape}" + ) + model_name = _scalar_str(data["model_name"]) if "model_name" in data else None + truncate_dim = _scalar_int(data["truncate_dim"]) if "truncate_dim" in data else None + embedding_dim = ( + _scalar_int(data["embedding_dim"]) if "embedding_dim" in data else None + ) + if embedding_dim is None: + embedding_dim = int(embeddings.shape[1]) + return VocabEmbeddingsCache( + words=words, + embeddings=embeddings, + model_name=model_name, + truncate_dim=truncate_dim, + embedding_dim=embedding_dim, + ) + + +def save_vocab_embeddings_npz( + npz_path: Path, + *, + words: list[str], + embeddings: np.ndarray, + model_name: str, + truncate_dim: int | None = None, +) -> None: + """Write compressed NPZ with forward-compatible metadata keys.""" + arr = np.asarray(embeddings, dtype=np.float32) + if arr.ndim != 2 or len(words) != arr.shape[0]: + raise ValueError(f"Cannot save NPZ: words={len(words)} embeddings={arr.shape}") + npz_path.parent.mkdir(parents=True, exist_ok=True) + payload: dict[str, Any] = { + "words": np.array(words, dtype=object), + "embeddings": arr, + "model_name": np.array(model_name), + "embedding_dim": np.array(int(arr.shape[1])), + } + if truncate_dim is not None: + payload["truncate_dim"] = np.array(int(truncate_dim)) + else: + # Explicit null-ish marker for readers (omit key = legacy unknown) + pass + np.savez_compressed(npz_path, **payload) + + +def _names_compatible(cached: str, hub_id: str) -> bool: + if cached == hub_id: + return True + return cached.split("/")[-1] == hub_id.split("/")[-1] + + +def npz_compatible_with_selection( + cache: VocabEmbeddingsCache, + selection: ModelSelection, +) -> tuple[bool, str | None]: + """ + Return (ok, reason). reason is set when incompatible. + + Checks Hub id (when present) and effective embedding width vs truncate_dim. + """ + if cache.model_name and not _names_compatible(cache.model_name, selection.hub_id): + return ( + False, + f"model_name={cache.model_name!r} != {selection.hub_id!r}", + ) + + expected_dim = selection.truncate_dim + actual_dim = int(cache.embeddings.shape[1]) + if expected_dim is not None and actual_dim != expected_dim: + return ( + False, + f"embedding width {actual_dim} != truncate_dim={expected_dim}", + ) + + if ( + cache.truncate_dim is not None + and selection.truncate_dim is not None + and cache.truncate_dim != selection.truncate_dim + ): + return ( + False, + f"npz truncate_dim={cache.truncate_dim} != {selection.truncate_dim}", + ) + + if ( + selection.truncate_dim is None + and cache.truncate_dim is not None + and cache.truncate_dim != actual_dim + ): + # Cached under a truncate that current selection no longer uses + return ( + False, + f"npz was truncated to {cache.truncate_dim} but selection has no truncate_dim", + ) + + return True, None diff --git a/backend/vocab_merge.py b/backend/vocab_merge.py new file mode 100644 index 0000000000000000000000000000000000000000..fe48f9701d1e288c2eeb7f70acfc69c0b4238bcb --- /dev/null +++ b/backend/vocab_merge.py @@ -0,0 +1,97 @@ +""" +EN∪ES vocabulary merge (multillm Slice 4). + +Languages beyond EN+ES are forbidden in this epic. Demo translation pairs from +roadmap §15 must appear in the merged file for cross-lingual lab demos. +""" + +from __future__ import annotations + +from pathlib import Path + +# Cross-lingual demo pairs (EN, ES) — must survive merge. +DEMO_PAIRS: tuple[tuple[str, str], ...] = ( + ("king", "rey"), + ("queen", "reina"), + ("man", "hombre"), + ("woman", "mujer"), + ("apple", "manzana"), + ("computer", "computadora"), + ("water", "agua"), + ("peace", "paz"), + ("dog", "perro"), + ("cat", "gato"), +) + +DEMO_WORDS: frozenset[str] = frozenset(w for pair in DEMO_PAIRS for w in pair) + +ALLOWED_LANG_TAGS = frozenset({"en", "es"}) + + +def normalize_word(raw: str) -> str | None: + word = raw.strip().lower() + if not word or word.startswith("#"): + return None + return word + + +def load_vocab_lines(path: Path) -> list[str]: + if not path.is_file(): + raise FileNotFoundError(f"Vocab file not found: {path}") + words: list[str] = [] + seen: set[str] = set() + with open(path, encoding="utf-8") as f: + for line in f: + word = normalize_word(line) + if word is None or word in seen: + continue + seen.add(word) + words.append(word) + return words + + +def merge_vocab_lists( + *lists: list[str], + ensure_words: frozenset[str] | None = None, +) -> list[str]: + """Lowercase dedupe preserving first-seen order; append any missing ensure_words.""" + merged: list[str] = [] + seen: set[str] = set() + for lst in lists: + for word in lst: + w = normalize_word(word) + if w is None or w in seen: + continue + seen.add(w) + merged.append(w) + required = ensure_words if ensure_words is not None else DEMO_WORDS + for word in sorted(required): + if word not in seen: + seen.add(word) + merged.append(word) + return merged + + +def merge_en_es_files( + en_path: Path, + es_path: Path, + out_path: Path, + *, + ensure_demo_pairs: bool = True, +) -> list[str]: + """Merge EN + ES vocab files → out_path (one word per line).""" + en_words = load_vocab_lines(en_path) + es_words = load_vocab_lines(es_path) + ensure = DEMO_WORDS if ensure_demo_pairs else frozenset() + merged = merge_vocab_lists(en_words, es_words, ensure_words=ensure) + out_path.parent.mkdir(parents=True, exist_ok=True) + with open(out_path, "w", encoding="utf-8") as f: + f.writelines(f"{word}\n" for word in merged) + return merged + + +def assert_demo_pairs_present(words: list[str] | set[str]) -> None: + present = {normalize_word(w) for w in words if normalize_word(w)} + missing = sorted(DEMO_WORDS - present) + if missing: + raise AssertionError(f"Merged vocab missing demo words: {missing}") diff --git a/current-research/DISCOVERY-shared-noise-embedding-geometry.md b/current-research/DISCOVERY-shared-noise-embedding-geometry.md new file mode 100644 index 0000000000000000000000000000000000000000..853a7d81c054e771d92f48f4c5c9586baf111b5e --- /dev/null +++ b/current-research/DISCOVERY-shared-noise-embedding-geometry.md @@ -0,0 +1,430 @@ +# Discovery Note — Shared Noise Is Embedding-Geometry Dependent + +**Status:** Research / evidence dump (not a product roadmap; not an implementation ticket) +**Date:** 2026-08-26 (UTC) +**Repo:** `vhectorlab` +**Authors of the session:** human (Hector) + coding agent (Cursor) +**Audience:** another AI (and the human) continuing analysis with full repo access + +**Location:** `current-research/` — empirical discoveries beyond day-to-day engineering lessons (`lessons-learned.md`). Do not treat this folder as a roadmap or as shipped product docs. + +This document captures an accidental discovery made while debugging why **Shared noise** felt alive on a local Mac lab but **did nothing** on the public Hugging Face Space. The first hypothesis was “HF is missing code / wrong wiring / model pin.” The deeper finding is: **the Shared noise pipeline is model-agnostic in code, but its *visibility* collapses or expands with the statistical geometry of the embedding space and the Compare batch.** + +Treat this as a primary source for follow-up science, not as a finished theory. + +--- + +## 0. How to use this file + +1. Read this document end-to-end before changing Shared noise math. +2. Cross-read (do not replace this file with them): + - `roadmap/HANDOFF-shared-noise.md` — product intent, SAE caveats, omit-common draft status + - `roadmap/shared-noise-omit-common.md` — agreed direction for *geometry* hide (not yet on `main`) + - `roadmap/archivo/shared-noise-coverage-knobs.md` — shipped 2.4.3 decisions (token-batch min/max) + - `src/visualizer/groupDimContrast.js` — the actual engine + - `tests/groupDimContrast.test.js` — locked formulas + - `.agents/skills/dev-protocol/lessons-learned.md` §8.9 — short engineering invariant distilled from this study +3. Re-run the reproduction snippets in §7 against whatever model is currently loaded locally / on the Space. +4. At the time of writing, **local** was already Arctic `local-full` @256; **HF Space** was still serving **mpnet @768** until a republish of Dockerfile commit `dbd822d` finishes building. Numbers below are from that split. + +--- + +## 1. The accident (chronology) + +### 1.1 Symptom the human reported + +> Shared noise works locally. On HF it does not move anything. Seriously — the knob did nothing. + +### 1.2 First-layer discrepancy (ops / deploy) + +Local `.env` (gitignored, observed 2026-08-26): + +| Knob | Local Mac | +| :--- | :--- | +| `MODEL_PROFILE` | `local-full` | +| `MODEL_NAME` | `Snowflake/snowflake-arctic-embed-m-v2.0` | +| `TRUNCATE_DIM` | `256` | +| `VOCAB_PATH` | `public/vocab_en_es.txt` | +| Device (`/health`) | `mps` | +| Embedding width | **256** | + +Hugging Face Space `hbauzan/llm-semantic-visualizer` **before** the Arctic pin (live `/health` during the study): + +| Knob | HF Space (cpu-basic) | +| :--- | :--- | +| Profile | `null` (no `MODEL_PROFILE`) | +| Model | `sentence-transformers/all-mpnet-base-v2` | +| Truncate | `null` (full width) | +| Vocab | `public/vocab.txt` (EN-only, from Dockerfile) | +| Device | `cpu` | +| Embedding width | **768** | + +Dockerfile historically hard-pinned `MODEL_NAME=all-mpnet-base-v2` for the cpu-basic demo epic. Multi-LLM catalog work explicitly deferred Space model policy (`roadmap/multillm/…`). So the Space was not “broken”; it was running a **different lab**. + +### 1.3 Second-layer question (this discovery) + +After explaining the model mismatch, the human asked: + +> Can Shared noise wiring work with one LLM but not another? Please review. + +That forced a code audit + measurements. **Result:** wiring is universal; **cancel rates are not.** + +### 1.4 Immediate product follow-up already landed + +Commit on `main`: `dbd822d` — `feat(hf): pin Space to local-full Arctic-m @256 + EN∪ES vocab`. + +Rationale checked against HF docs: **cpu-basic** = 2 vCPU / **16 GB RAM** / **50 GB** disk. Arctic `model.safetensors` ≈ **1.2 GB** → fits. `trust_remote_code` + xformers disable / GTE buffer repair already live in `backend/model_catalog.build_model` (lesson 8.8). + +After option **8** republish + Space rebuild, `/health` should report Arctic · 256D · `model_profile: local-full`. Until then, comparative measurements against HF still see mpnet. + +--- + +## 2. What Shared noise *is* on `main` (engineering contract) + +### 2.1 Scope + +- **UI:** Visualization panel → “Shared noise” toggle + Similarity knobs (`A` / `mA`). +- **Gate:** ≥2 Compare tokens with equal-width embeddings (`hasEnoughTokensForSharedNoise`). Groups are **not** required for Shared noise (Group contrast / Sign conflict is a separate gate). +- **Effect:** **Paint only** toward black (and alpha fade). Thread **Y / geometry / undulation unchanged** on `main`. +- **Not shipped:** omit-common / per-word holes / geometry hide (`feat/shared-noise-per-word` never merged; omit-common still draft). + +### 2.2 Metric (token-batch, since 2.4.3) + +For each dimension `d` over all Compare items with embeddings: + +``` +min_d = min_i embedding_i[d] +max_d = max_i embedding_i[d] +sameSign_d = signedUnit(min_d) == signedUnit(max_d) # signedUnit: v >= 0 → +1 (zero counts as +) +similarity_d = 1 - |max_d - min_d| / (|max_d| + |min_d|) # both ~0 → 1 +``` + +**Critical veto (D4):** if `sameSign_d` is false → **no Shared-noise cancel on that dim**, regardless of Similarity knob. One opposite-sign outlier vetoes the entire dimension for the whole batch. + +### 2.3 Cancel amount (knob) + +``` +coverage01 = highCoverageToUnit(sameSignCancelCoverage) # UI percent → [0,1] +cancel_d = cancelAmountFromMetric(similarity_d, coverage01) +# floor = 1 - coverage01; if similarity <= floor → 0; else ramp to 1 as similarity → 1 +``` + +Default Similarity coverage sits at the high-coverage minimum (historically “30%” class floor for high-coverage knobs — see `DEFAULT_HIGH_COVERAGE` in `visualizationControlsDefaults.js`). Human often runs ~70–80% when exploring. + +### 2.4 Where it is applied + +| Stage | File | Role | +| :--- | :--- | :--- | +| Metrics | `src/visualizer/groupDimContrast.js` → `computeTokenSharedNoiseMetrics` | min/max / sameSign / similarity | +| Weights | `paintWeightsForDim` | cancel from token metrics; highlight from G1↔G2 group metrics | +| POINTS attrs | `buildPointGroupPaintAttributes` | per-point cancel/highlight using `meta.dim` = **source** dim | +| Color | `groupHuePaint.js` → `applyGroupDimPaint` | lerp toward black / highlight | +| Instancer | `src/visualizer/Instancer.js` | computes token metrics on **raw** item embeddings; dim-sort permutation tracked via `sourceDims` so paint still indexes original dims | + +There is **no** branch on `MODEL_NAME`, `embedding_dim`, profile id, or Hub id in this path. Inputs are `number[]` embeddings already present on Compare items (RAW or whatever the UI currently visualizes after SAE encode — if SAE is ON, the visualized vectors are SAE features, not the encoder dims; see handoff SAE warnings). + +### 2.5 Scale invariance of similarity (important) + +For same-sign pairs, `sharedNoiseSimilarity(a,b)` depends on **relative** spread, not absolute magnitude: + +- `(0.01, 0.02)` → similarity `0.667` +- `(0.10, 0.20)` → similarity `0.667` + +So “mpnet has smaller float magnitudes” is **not** by itself an explanation. What matters is: + +1. How often min/max **straddle zero** across the batch (veto). +2. Conditional on same sign, how flat min≈max is across tokens. + +Those are properties of **embedding geometry + batch composition**, not of the JS wiring. + +--- + +## 3. The discovery (compressed statement) + +**Claim.** For the same Shared noise code and the same Compare texts, **all-mpnet-base-v2 @768** produces far fewer dimensions that survive the same-sign veto and receive non-zero cancel than **Snowflake arctic-embed-m-v2.0 Matryoshka-truncated @256**. On diverse batches, mpnet cancel can fall to a few percent of dims — visually “the knob does nothing” in POINTS — while Arctic still paints a meaningful fraction of the thread. + +**Non-claim.** We do **not** claim Arctic is “better” in general, nor that Shared noise is “correct” on Arctic. We claim the **current metric is hypersensitive to sign-unanimity across the batch**, and different encoders + widths change how often that unanimity occurs. + +**Accident value.** The HF vs local mismatch was the instrument that made the hypersensitivity obvious. Without two live endpoints running different models on the same UI, it looked like a deploy bug. + +--- + +## 4. Evidence A — synthetic geometry (agent simulation) + +Quick Monte Carlo (Python, `random.gauss` isotropic vectors then L2-normalize). Coverage = `0.77`. + +| Scenario | Dims | Tokens | sameSign % | cancel>0 @77% | +| :--- | ---: | ---: | ---: | ---: | +| Isotropic L2 (mpnet-like) | 768 | 12 | **0%** | **0** | +| Isotropic L2 (arctic-width) | 256 | 12 | **0%** | **0** | +| Near-duplicate tokens (noise 0.15 on shared base) | 768 | 4 | ~88% | many | +| 3-group bootstrap with opposing group base | 768 | 24 | **0%** | **0** | +| 2 groups, all-positive correlated | 768 | 12 | ~98% | many | +| Structured: first 80 dims shared, rest noise (256-D) | 256 | 12 | ~31% | 80 dims | + +**Interpretation:** + +- Pure isotropic high-dim L2 spaces almost never keep unanimous signs across ≥12 diverse tokens → Shared noise is dead by construction. +- Real encoders are **not** isotropic; they have structure. The question becomes *how much same-sign flatness survives for a given batch*. +- The classic 3-group “happy / sad / angry” style opposition annihilates token-batch Shared noise even before model choice (angry vetoes dims that happy & sad shared). This matches the archived roadmap note that 2.4.3 with large Compare bootstraps “almost doesn’t fire.” + +--- + +## 5. Evidence B — live `/compare` on the same 12 texts + +### 5.1 Probe texts (fixed) + +```json +["happy","joyful","glad","sad","unhappy","sorrow","angry","furious","mad","car","truck","bike"] +``` + +Mixed emotions + vehicles — intentional diversity (closer to a small multi-theme Compare than to three synonyms). + +### 5.2 Endpoints + +| Endpoint | Observed `/health` during measurement | +| :--- | :--- | +| Local | `Snowflake/snowflake-arctic-embed-m-v2.0`, profile `local-full`, **256-D**, truncate 256, device `mps` | +| HF Space | `sentence-transformers/all-mpnet-base-v2`, profile `null`, **768-D**, device `cpu` | + +POST local: `http://127.0.0.1:8000/compare` +POST Space: `https://hbauzan-llm-semantic-visualizer.hf.space/api/compare` + +### 5.3 Results (Shared noise math applied offline to returned embeddings) + +Coverage = **0.77** (Similarity knob ≈ 77%). + +| Batch | Model | Dim | sameSign dims | sameSign % | sim>0.9 | cancel>0 | cancel>0.5 | +| :--- | :--- | ---: | ---: | ---: | ---: | ---: | ---: | +| Mixed 12 | **Arctic local** | 256 | 77 | **30.1%** | 0 | **55** | 10 | +| Mixed 12 | **mpnet HF** | 768 | 52 | **6.8%** | 4 | **24** | 4 | +| Happy synonyms (3) | Arctic local | 256 | 194 | 75.8% | 19 | 171 | 106 | +| Happy synonyms (3) | mpnet HF | 768 | 382 | 49.7% | 12 | 317 | 135 | +| Emotion 9 (no cars) | Arctic local | 256 | 104 | 40.6% | 0 | 75 | 18 | + +### 5.4 Why this matches the human’s perception + +- POINTS draws one point per (token × dim). Paint cancel only on dims that pass the veto and clear the coverage floor. +- **Arctic mixed-12:** ~**21%** of dims get *some* cancel (`55/256`). Visible darkening along threads. +- **mpnet mixed-12:** ~**3.1%** of dims get some cancel (`24/768`). On a 768-point-long thread, blackening ~24 indices is easy to miss → “knob does nothing.” +- On **near synonyms**, both models respond. So Shared noise is not “broken on mpnet”; it is **batch- and geometry-starved** on diverse mpnet Compare scenes. + +### 5.5 Coverage = 1.0 (full Similarity) on mixed-12 + +| Model | cancel>0 at 100% Similarity | +| :--- | ---: | +| Arctic | 77 (all sameSign dims) | +| mpnet | 52 (all sameSign dims) | + +Even at max knob, mpnet only has **52** candidate dims vs Arctic’s **77**, on a canvas that is **3× wider** (768 vs 256). Density of affected points ≈ `52/768 ≈ 6.8%` vs `77/256 ≈ 30%`. + +--- + +## 6. What we ruled out + +| Hypothesis | Verdict | +| :--- | :--- | +| HF missing Shared noise frontend | **Ruled out.** Same `main` UI; Space served Shared noise controls. | +| JS wiring keyed on model id / dim | **Ruled out.** No such branches in `groupDimContrast.js` / Instancer paint path. | +| Similarity formula fails on small magnitudes | **Ruled out** for same-sign case (scale-invariant). | +| Dim-sort permutation breaks paint index | **Unlikely as HF-only cause.** Instancer stores `meta.dim = sourceDim`; metrics indexed by source dim. Would affect both models equally if buggy. | +| “Must use SAE” / “must use RAW” as HF difference | **Not required for the finding.** Measurements used `/compare` RAW embeddings. SAE ON would change the visualized space further (handoff warns SAE Top‑K fights “hide the common pack”). | +| Disk / Docker / option 7–8 publish bugs | **Separate saga** (GIF binary + orphan tip + full disk). They delayed republish but did not invent the Shared noise geometry effect. | + +--- + +## 7. Reproduction recipes (for the next AI) + +### 7.1 Offline metric helper (Python) + +```python +def shared_noise_similarity(a, b): + aa, bb = abs(a), abs(b) + den = aa + bb + if den <= 1e-12: + return 1.0 + return max(0.0, min(1.0, 1.0 - abs(a - b) / den)) + +def cancel_amount_from_metric(metric01, coverage01): + m = max(0.0, min(1.0, float(metric01))) + c = max(0.0, min(1.0, float(coverage01))) + if c <= 1e-12: + return 0.0 + floor = 1.0 - c + if m <= floor: + return 0.0 + return (m - floor) / c + +def analyze_batch(embeddings, coverage=0.77): + """embeddings: list[list[float]] shape (n_tokens, dim)""" + n, d = len(embeddings), len(embeddings[0]) + same = hi = c0 = c5 = 0 + for di in range(d): + vals = [embeddings[i][di] for i in range(n)] + lo, hi_ = min(vals), max(vals) + same_sign = (lo >= 0) == (hi_ >= 0) # matches signedUnit: 0 → + + sim = shared_noise_similarity(lo, hi_) + if same_sign: + same += 1 + if sim > 0.9: + hi += 1 + c = cancel_amount_from_metric(sim, coverage) + if c > 1e-9: + c0 += 1 + if c > 0.5: + c5 += 1 + return { + "n": n, + "d": d, + "same_sign": same, + "same_sign_pct": 100.0 * same / d, + "sim_gt_0_9": hi, + "cancel_gt_0": c0, + "cancel_gt_0_5": c5, + } +``` + +### 7.2 Fetch embeddings + +```bash +# Local +curl -sS -X POST 'http://127.0.0.1:8000/compare' \ + -H 'Content-Type: application/json' \ + -d '{"texts":["happy","joyful","glad","sad","unhappy","sorrow","angry","furious","mad","car","truck","bike"]}' \ + -o /tmp/cmp_local.json + +# Space (path may be /compare or /api/compare depending on reverse proxy; Space uses /api) +curl -sS -X POST 'https://hbauzan-llm-semantic-visualizer.hf.space/api/compare' \ + -H 'Content-Type: application/json' \ + -d '{"texts":["happy","joyful","glad","sad","unhappy","sorrow","angry","furious","mad","car","truck","bike"]}' \ + -o /tmp/cmp_hf.json + +curl -sS 'http://127.0.0.1:8000/health' +curl -sS 'https://hbauzan-llm-semantic-visualizer.hf.space/health' +``` + +Parse `items[].embedding` from the JSON and run `analyze_batch`. + +### 7.3 Vitest ground truth + +```bash +npx vitest run tests/groupDimContrast.test.js +``` + +Formulas and veto examples are locked there (including “G3 outlier blocks high sim”). + +### 7.4 UI ground truth + +1. COMPARE + ANALYSIS + POINTS. +2. Prefer **SAE OFF** for this study (RAW floats). +3. Enable Shared noise; sweep Similarity. +4. Repeat with (a) 3 near synonyms, (b) mixed 12 probe, (c) full bootstrap (`it_core` + `vehicles` + `women` — hundreds of tokens). Expect (c) to starve cancel on **both** models. + +--- + +## 8. Related product context (do not confuse layers) + +### 8.1 Already known before this discovery + +From `roadmap/HANDOFF-shared-noise.md`: + +- 2.4.3 retarget to token-batch min/max made large Compare bootstraps look “dead.” +- Zero coverage is a **different** instrument (colormap band around 0). +- Desired future: hide the **pack** in **geometry** (omit-common), keep the distinctive point — **not** paint-only. + +This discovery **sharpens** that story: even when the batch is moderate (12 tokens), **encoder choice** can push you into the “dead knob” regime. + +### 8.2 SAE trap (repeat for the next AI) + +Lab SAE = dictionary fit on current batch + Top‑K **largest** activations for reconstruction. Shared large features survive; unique small ones die. Yellow stripes across groups with SAE ON are often **not** Shared noise failing — they are SAE doing its job. Study Shared noise on **RAW** first. + +### 8.3 Matryoshka / truncate + +Arctic `local-full` uses `TRUNCATE_DIM=256` (catalog default). That is not cosmetic: it selects a Matryoshka prefix. Whether the **prefix** is more same-sign-stable across diverse tokens than full 768 mpnet is an open scientific question suggested by the data, not proven as “MRL causes X.” + +--- + +## 9. Open questions for deeper analysis + +Prioritize with the human; do not silently implement. + +1. **Is Arctic@256 systematically more same-sign-stable than mpnet@768 across many batches?** + Need a grid: synonym sets, antonym sets, multi-theme, full bootstrap, random vocab samples. Report distributions of `sameSign%` and `cancel>0` density (normalize by dim). + +2. **Width vs model family vs MRL prefix** + Ablate: Arctic@256 vs Arctic@768 (if quality allows) vs MiniLM-multi@384 (`local-comfort` / old `hf-demo` preset) vs mpnet@768 on identical texts. + +3. **Is unanimous sign the wrong veto?** + Product intent (“hide the shared pack, keep the odd one out”) fights hard with min/max + sameSign. One opposite token currently **zeros the whole dim**. That may be correct for “common mode,” or it may be the bug relative to human vision (L2 in omit-common draft: 229 equal + 1 different → hide the 229, keep the 1). + +4. **Per-dim robust statistics** + Alternatives to min/max: trimmed min/max, percentiles, MAD, “mode cluster” mass, pairwise agreement rates, ignore bottom-k outliers. Measure cancel density vs false hide of distinctive points. + +5. **Normalize visibility by dim count** + UI perception ≈ fraction of points painted, not absolute count. A metric “cancel mass” = `mean(cancel_d)` or `sum(cancel_d)/D` may predict “knob feels alive” better than raw counts. + +6. **Interaction with L2 normalization** + Sentence-transformers typically L2-normalize. Does that increase zero-straddling vs unnormalized Matryoshka prefixes? + +7. **After Space rebuild** + Re-run §5 against HF once `/health` shows Arctic@256. Expect HF ≈ local for the same texts (device noise aside). If not, look for SAE defaults, vocab-only differences, or stale frontend bundle. + +8. **Documentation / teaching** + Should the UI warn when `sameSign%` is near zero for the current batch (“Shared noise has almost no candidate dims on this Compare set / model”)? + +--- + +## 10. Practical guidance (until the science settles) + +- To **demo Shared noise** the way the Mac lab feels: use **Arctic `local-full` @256** (Space Dockerfile now aims there) and start with **small, coherent token sets** before the giant bootstrap. +- To **stress-test** the metric: use multi-theme or 3-group opposing batches — expect starvation by design. +- Do **not** assume HF is wrong if a knob looks dead on mpnet with diverse Compare; measure `sameSign%` first. +- Do **not** “fix” Shared noise by special-casing model ids without an explicit product decision. + +--- + +## 11. Code & commit pointers (repo anchors) + +| Item | Location | +| :--- | :--- | +| Engine | `src/visualizer/groupDimContrast.js` | +| Tests | `tests/groupDimContrast.test.js` | +| Instancer wiring | `src/visualizer/Instancer.js` (`computeTokenSharedNoiseMetrics`, `sourceDims`) | +| Paint | `src/visualizer/groupHuePaint.js`, `applyGroupDimPaint` | +| UI / storage | `src/ui/VisualizationControls.js`, `src/ui/visualizationControlsDefaults.js` | +| Gate from main | `src/main.js` (`setSharedNoiseControlsEnabled`, `hasEnoughTokensForSharedNoise`) | +| Catalog profiles | `backend/model_catalog.py` (`local-full` → Arctic + default truncate 256) | +| Space pin | `Dockerfile` (`MODEL_PROFILE=local-full`, `TRUNCATE_DIM=256`, `vocab_en_es.txt`) | +| Space card blurb | `deploy/hf/space-frontmatter.yml` | +| Arctic/xformers lesson | `.agents/skills/dev-protocol/lessons-learned.md` §8.8 | +| HF packaging lesson | same file §8.5 (+ Space model note 2026-08) | +| Commit pinning Space model | `dbd822d` | +| Evidence dump (this study) | `current-research/DISCOVERY-shared-noise-embedding-geometry.md` | +| Lesson distilled | `.agents/skills/dev-protocol/lessons-learned.md` §8.9 | +| Product handoff | `roadmap/HANDOFF-shared-noise.md` | +| Omit-common draft | `roadmap/shared-noise-omit-common.md` | + +--- + +## 12. Suggested next session prompt (paste for another AI) + +```text +Read current-research/DISCOVERY-shared-noise-embedding-geometry.md (this file). +Also read roadmap/HANDOFF-shared-noise.md, lessons-learned.md §8.9, and src/visualizer/groupDimContrast.js. +Do NOT implement omit-common or change Shared noise math unless I explicitly ask. +First: verify current /health on local and on the HF Space. +Re-run the §5 probe batch and report sameSign% and cancel density for whatever models are live. +Then help me design an ablation grid (models × batch types) to test whether Matryoshka-256 +systematically preserves more same-sign flat dims than mpnet-768, and whether the unanimous-sign +veto conflicts with the “229 same + 1 different → keep the one” product intent. +``` + +--- + +## 13. One-paragraph abstract + +While aligning a Hugging Face Space with a local VHectorLab setup, we found that Shared noise—a paint-only effect that blackens dimensions whose values are unanimously signed and mutually similar across Compare tokens—can appear completely inert on `all-mpnet-base-v2` (768-D) for diverse token batches, yet clearly active on `Snowflake/snowflake-arctic-embed-m-v2.0` truncated to 256-D for the **same** texts and the **same** frontend code. Live measurements showed ~30% same-sign dims and ~21% dims with cancel>0 on Arctic versus ~7% same-sign and ~3% cancel>0 on mpnet at 77% Similarity coverage. The code path is model-agnostic; the metric’s hard same-sign veto and min/max batch statistics make perceived “brokenness” a function of embedding geometry and batch diversity. The Space has since been pinned toward the local-full Arctic profile so demos match the Mac lab; deeper metric redesign remains an open research/product question tied to the omit-common roadmap. + +--- + +*End of discovery note. Append new measurement tables below rather than rewriting history; date each addendum.* diff --git a/current-research/README.md b/current-research/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c4a9f7d7b38ac0c48d57bf91e38aa3971d9d35ae --- /dev/null +++ b/current-research/README.md @@ -0,0 +1,17 @@ +# current-research/ + +Empirical **discoveries** that go beyond day-to-day engineering lessons. + +| Folder / file | Role | +| :--- | :--- | +| **`current-research/`** | Long-form evidence dumps, measurement notes, open scientific questions. Safe for another AI + human to continue analysis. | +| **`.agents/skills/dev-protocol/lessons-learned.md`** | Short **invariants** agents must not re-break while coding. | +| **`roadmap/`** | Product intent, drafts, handoffs, implementation tickets. | + +Do **not** treat a discovery note as a license to change shipped math or UI unless the human explicitly asks for an implementation pass (dev-protocol). + +## Index + +| Note | Topic | +| :--- | :--- | +| [`DISCOVERY-shared-noise-embedding-geometry.md`](./DISCOVERY-shared-noise-embedding-geometry.md) | Shared noise paint looks “dead” on mpnet@768 vs alive on Arctic@256 for the same texts — geometry/veto, not model-specific wiring (2026-08-26). | diff --git a/deploy/hf/README.md b/deploy/hf/README.md new file mode 100644 index 0000000000000000000000000000000000000000..397f53048c7c6d53a88d24a230f1e20074efa1b2 --- /dev/null +++ b/deploy/hf/README.md @@ -0,0 +1,28 @@ +# Hugging Face Space publish assets + +GitHub keeps a normal project `README.md` (no Space YAML). + +HF Spaces still require YAML frontmatter on the Space repo’s root `README.md` +(`sdk: docker`, `app_port: 7860`, …). See +[Spaces Configuration Reference](https://huggingface.co/docs/hub/spaces-config-reference). + +## Files + +| File | Role | +| :--- | :--- | +| `space-frontmatter.yml` | Canonical Space card / Docker contract (fenced with `---`) | + +## How it is applied + +`./setup.sh` → **option 8** (`publish_hf_space`): + +1. Builds production frontend (smoke). +2. Composes `frontmatter + README.md` body via `scripts/compose_hf_space_readme.sh` (drops local `demo/*` media embeds). +3. Creates an **orphan** ephemeral commit with `commit-tree` (no parent — so GitHub history with demo GIFs is not packed), stripping `demo/*` / gifs from the tree. Working tree / GitHub `main` unchanged. +4. Force-pushes that orphan tip to the Space remote (always `--force`; orphan cannot fast-forward). + +Linux image builds use `torch` from the PyTorch **cpu** wheel index (`backend/pyproject.toml` `[tool.uv.sources]`), not CUDA/`nvidia-*` from PyPI. + +**Space model (cpu-basic):** `MODEL_PROFILE=local-full` → `Snowflake/snowflake-arctic-embed-m-v2.0` with `TRUNCATE_DIM=256` and `public/vocab_en_es.txt`. Weights ~1.2 GB; fits cpu-basic (16 GB RAM / 50 GB disk). Build downloads the Hub model and precomputes the vocab NPZ (several minutes). + +Override path with env `HF_SPACE_FRONTMATTER` (default: `deploy/hf/space-frontmatter.yml`). diff --git a/deploy/hf/space-frontmatter.yml b/deploy/hf/space-frontmatter.yml new file mode 100644 index 0000000000000000000000000000000000000000..9a00083a43191b70838a079bef1c0356e392ba50 --- /dev/null +++ b/deploy/hf/space-frontmatter.yml @@ -0,0 +1,11 @@ +--- +title: VHectorLab 3D +emoji: 🧭 +colorFrom: gray +colorTo: blue +sdk: docker +app_port: 7860 +pinned: false +license: apache-2.0 +short_description: VHectorLab 3D - Semantic embeddings study tool in WebGL +--- diff --git a/index.html b/index.html new file mode 100644 index 0000000000000000000000000000000000000000..e41e1f4d76047f3f52d58f74af389ed596b5b824 --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + VHectorLab 3D - 3D Semantic Vector Arithmetic Visualizer + + + + +
+ + + diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..c02f7f16e2c18008fb4c3f35f35c1419e2a0be27 --- /dev/null +++ b/manifest.json @@ -0,0 +1,18 @@ +{ + "project": "vhectorlab-3d", + "version": "3.0.1", + "bootstrap_run": false, + "state_schema": { + "model_name": "all-mpnet-base-v2", + "vocab_path": "public/vocab.txt", + "host": "127.0.0.1", + "port": 8000 + }, + "constraints": { + "vector_dim": 768, + "cors_allow_origins": [ + "*" + ], + "cors_allow_credentials": false + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..094001fb561556b65511ac7c89854a8ebd9a2e13 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1931 @@ +{ + "name": "vhectorlab-3d", + "version": "2.2.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vhectorlab-3d", + "version": "2.2.1", + "dependencies": { + "three": "^0.162.0" + }, + "devDependencies": { + "@types/three": "^0.162.0", + "vite": "^5.1.6", + "vitest": "^1.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.12", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.162.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.162.0.tgz", + "integrity": "sha512-0j5yZcVukVIhrhSIC7+LmBPkkMoMuEJ1AfYBZfgNytdYqYREMuiyXWhYOMeZLBElTEAlJIZn7r2W3vqTIgjWlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tweenjs/tween.js": "~23.1.1", + "@types/stats.js": "*", + "@types/webxr": "*", + "fflate": "~0.6.10", + "meshoptimizer": "~0.18.1" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fflate": { + "version": "0.6.11", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.11.tgz", + "integrity": "sha512-3JyEFWGjFn7zHmoa9+zG1BmW7X2okcmAB+0Cnu9UFbVs/jCBnl2A8o065ZlXiw145K3eBM3uLuzrYXC0RK7eDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/meshoptimizer": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", + "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/three": { + "version": "0.162.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.162.0.tgz", + "integrity": "sha512-xfCYj4RnlozReCmUd+XQzj6/5OjDNHBy5nT6rVwrOKGENAvpXe2z1jL+DZYaMu4/9pNsjH/4Os/VvS9IrH7IOQ==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..f38b1226ad3705693dd54ffb24eee9ce5d2af20e --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "vhectorlab-3d", + "version": "3.0.1", + "license": "Apache-2.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "test": "vitest run" + }, + "dependencies": { + "three": "^0.162.0" + }, + "devDependencies": { + "@types/three": "^0.162.0", + "vite": "^5.1.6", + "vitest": "^1.4.0" + } +} diff --git a/public/vocab.txt b/public/vocab.txt new file mode 100644 index 0000000000000000000000000000000000000000..448a9592c2aa64158fa8fe9343b81129cbbb6620 --- /dev/null +++ b/public/vocab.txt @@ -0,0 +1,9858 @@ +the +of +and +to +in +for +is +on +that +by +this +with +you +it +not +or +be +are +from +at +as +your +all +have +new +more +an +was +we +will +home +can +us +about +if +page +my +has +search +free +but +our +one +other +do +no +information +time +they +site +he +up +may +what +which +their +news +out +use +any +there +see +only +so +his +when +contact +here +business +who +web +also +now +help +get +pm +view +online +first +am +been +would +how +were +me +services +some +these +click +its +like +service +than +find +price +date +back +top +people +had +list +name +just +over +state +year +day +into +email +two +health +world +re +next +used +go +work +last +most +products +music +buy +data +make +them +should +product +system +post +her +city +add +policy +number +such +please +available +copyright +support +message +after +best +software +then +jan +good +video +well +where +info +rights +public +books +high +school +through +each +links +she +review +years +order +very +privacy +book +items +company +read +group +need +many +user +said +de +does +set +under +general +research +university +january +mail +full +map +reviews +program +life +know +games +way +days +management +part +could +great +united +hotel +real +item +international +center +ebay +must +store +travel +comments +made +development +report +off +member +details +line +terms +before +hotels +did +send +right +type +because +local +those +using +results +office +education +national +car +design +take +posted +internet +address +community +within +states +area +want +phone +dvd +shipping +reserved +subject +between +forum +family +long +based +code +show +even +black +check +special +prices +website +index +being +women +much +sign +file +link +open +today +technology +south +case +project +same +pages +uk +version +section +own +found +sports +house +related +security +both +county +american +photo +game +members +power +while +care +network +down +computer +systems +three +total +place +end +following +download +him +without +per +access +think +north +resources +current +posts +big +media +law +control +water +history +pictures +size +art +personal +since +including +guide +shop +directory +board +location +change +white +text +small +rating +rate +government +children +during +usa +return +students +shopping +account +times +sites +level +digital +profile +previous +form +events +love +old +john +main +call +hours +image +department +title +description +non +insurance +another +why +shall +property +class +cd +still +money +quality +every +listing +content +country +private +little +visit +save +tools +low +reply +customer +december +compare +movies +include +college +value +article +york +man +card +jobs +provide +food +source +author +different +press +learn +sale +around +print +course +job +canada +process +teen +room +stock +training +too +credit +point +join +science +men +categories +advanced +west +sales +look +english +left +team +estate +box +conditions +select +windows +photos +gay +thread +week +category +note +live +large +gallery +table +register +however +june +october +november +market +library +really +action +start +series +model +features +air +industry +plan +human +provided +tv +yes +required +second +hot +accessories +cost +movie +forums +march +la +september +better +say +questions +july +yahoo +going +medical +test +friend +come +dec +server +pc +study +application +cart +staff +articles +san +feedback +again +play +looking +issues +april +never +users +complete +street +topic +comment +financial +things +working +against +standard +tax +person +below +mobile +less +got +blog +party +payment +equipment +login +student +let +programs +offers +legal +above +recent +park +stores +side +act +problem +red +give +memory +performance +social +august +quote +language +story +sell +options +experience +rates +create +key +body +young +america +important +field +few +east +paper +single +ii +age +activities +club +example +girls +additional +password +latest +something +road +gift +question +changes +night +ca +hard +texas +oct +pay +four +poker +status +browse +issue +range +building +seller +court +february +always +result +audio +light +write +war +nov +offer +blue +groups +al +easy +given +files +event +release +analysis +request +fax +china +making +picture +needs +possible +might +professional +yet +month +major +star +areas +future +space +committee +hand +sun +cards +problems +london +washington +meeting +rss +become +interest +id +child +keep +enter +california +share +similar +garden +schools +million +added +reference +companies +listed +baby +learning +energy +run +delivery +net +popular +term +film +stories +put +computers +journal +reports +co +try +welcome +central +images +president +notice +original +head +radio +until +cell +color +self +council +away +includes +track +australia +discussion +archive +once +others +entertainment +agreement +format +least +society +months +log +safety +friends +sure +faq +trade +edition +cars +messages +marketing +tell +further +updated +association +able +having +provides +david +fun +already +green +studies +close +common +drive +specific +several +gold +feb +living +sep +collection +called +short +arts +lot +ask +display +limited +powered +solutions +means +director +daily +beach +past +natural +whether +due +et +electronics +five +upon +period +planning +database +says +official +weather +mar +land +average +done +technical +window +france +pro +region +island +record +direct +microsoft +conference +environment +records +st +district +calendar +costs +style +url +front +statement +update +parts +aug +ever +downloads +early +miles +sound +resource +present +applications +either +ago +document +word +works +material +bill +apr +written +talk +federal +hosting +rules +final +adult +tickets +thing +centre +requirements +via +cheap +kids +finance +true +minutes +else +mark +third +rock +gifts +europe +reading +topics +bad +individual +tips +plus +auto +cover +usually +edit +together +videos +percent +fast +function +fact +unit +getting +global +tech +meet +far +economic +en +player +projects +lyrics +often +subscribe +submit +germany +amount +watch +included +feel +though +bank +risk +thanks +everything +deals +various +words +linux +jul +production +commercial +james +weight +town +heart +advertising +received +choose +treatment +newsletter +archives +points +knowledge +magazine +error +camera +jun +girl +currently +construction +toys +registered +clear +golf +receive +domain +methods +chapter +makes +protection +policies +loan +wide +beauty +manager +india +position +taken +sort +listings +models +michael +known +half +cases +step +engineering +florida +simple +quick +none +wireless +license +paul +friday +lake +whole +annual +published +later +basic +sony +shows +corporate +google +church +method +purchase +customers +active +response +practice +hardware +figure +materials +fire +holiday +chat +enough +designed +along +among +death +writing +speed +html +countries +loss +face +brand +discount +higher +effects +created +remember +standards +oil +bit +yellow +political +increase +advertise +kingdom +base +near +environmental +thought +stuff +french +storage +oh +japan +doing +loans +shoes +entry +stay +nature +orders +availability +africa +summary +turn +mean +growth +notes +agency +king +monday +european +activity +copy +although +drug +pics +western +income +force +cash +employment +overall +bay +river +commission +ad +package +contents +seen +players +engine +port +album +regional +stop +supplies +started +administration +bar +institute +views +plans +double +dog +build +screen +exchange +types +soon +sponsored +lines +electronic +continue +across +benefits +needed +season +apply +someone +held +ny +anything +printer +condition +effective +believe +organization +effect +asked +eur +mind +sunday +selection +casino +pdf +lost +tour +menu +volume +cross +anyone +mortgage +hope +silver +corporation +wish +inside +solution +mature +role +rather +weeks +addition +came +supply +nothing +certain +usr +executive +running +lower +necessary +union +jewelry +according +dc +clothing +mon +com +particular +fine +names +robert +homepage +hour +gas +skills +six +bush +islands +advice +career +military +rental +decision +leave +british +teens +pre +huge +sat +woman +facilities +zip +bid +kind +sellers +middle +move +cable +opportunities +taking +values +division +coming +tuesday +object +lesbian +appropriate +machine +logo +length +actually +nice +score +statistics +client +ok +returns +capital +follow +sample +investment +sent +shown +saturday +christmas +england +culture +band +flash +ms +lead +george +choice +went +starting +registration +fri +thursday +courses +consumer +hi +airport +foreign +artist +outside +furniture +levels +channel +letter +mode +phones +ideas +wednesday +structure +fund +summer +allow +degree +contract +button +releases +wed +homes +super +male +matter +custom +virginia +almost +took +located +multiple +asian +distribution +editor +inn +industrial +cause +potential +song +cnet +ltd +los +hp +focus +late +fall +featured +idea +rooms +female +responsible +inc +communications +win +associated +thomas +primary +cancer +numbers +reason +tool +browser +spring +foundation +answer +voice +eg +friendly +schedule +documents +communication +purpose +feature +bed +comes +police +everyone +independent +ip +approach +cameras +brown +physical +operating +hill +maps +medicine +deal +hold +ratings +chicago +forms +glass +happy +tue +smith +wanted +developed +thank +safe +unique +survey +prior +telephone +sport +ready +feed +animal +sources +mexico +population +pa +regular +secure +navigation +operations +therefore +simply +evidence +station +christian +round +paypal +favorite +understand +option +master +valley +recently +probably +thu +rentals +sea +built +publications +blood +cut +worldwide +improve +connection +publisher +hall +larger +anti +networks +earth +parents +nokia +impact +transfer +introduction +kitchen +strong +tel +carolina +wedding +properties +hospital +ground +overview +ship +accommodation +owners +disease +tx +excellent +paid +italy +perfect +hair +opportunity +kit +classic +basis +command +cities +william +express +award +distance +tree +peter +assessment +ensure +thus +wall +ie +involved +el +extra +especially +interface +partners +budget +rated +guides +success +maximum +ma +operation +existing +quite +selected +boy +amazon +patients +restaurants +beautiful +warning +wine +locations +horse +vote +forward +flowers +stars +significant +lists +technologies +owner +retail +animals +useful +directly +manufacturer +ways +est +son +providing +rule +mac +housing +takes +iii +gmt +bring +catalog +searches +max +trying +mother +authority +considered +told +xml +traffic +programme +joined +input +strategy +feet +agent +valid +bin +modern +senior +ireland +teaching +door +grand +testing +trial +charge +units +instead +canadian +cool +normal +wrote +enterprise +ships +entire +educational +md +leading +metal +positive +fl +fitness +chinese +opinion +mb +asia +football +abstract +uses +output +funds +mr +greater +likely +develop +employees +artists +alternative +processing +responsibility +resolution +java +guest +seems +publication +pass +relations +trust +van +contains +session +multi +photography +republic +fees +components +vacation +century +academic +assistance +completed +skin +graphics +indian +prev +ads +mary +il +expected +ring +grade +dating +pacific +mountain +organizations +pop +filter +mailing +vehicle +longer +consider +int +northern +behind +panel +floor +german +buying +match +proposed +default +require +iraq +boys +outdoor +deep +morning +otherwise +allows +rest +protein +plant +reported +hit +transportation +mm +pool +mini +politics +partner +disclaimer +authors +boards +faculty +parties +fish +membership +mission +eye +string +sense +modified +pack +released +stage +internal +goods +recommended +born +unless +richard +detailed +japanese +race +approved +background +target +except +character +usb +maintenance +ability +maybe +functions +ed +moving +brands +places +php +pretty +trademarks +phentermine +spain +southern +yourself +etc +winter +battery +youth +pressure +submitted +boston +debt +keywords +medium +television +interested +core +break +purposes +throughout +sets +dance +wood +msn +itself +defined +papers +playing +awards +fee +studio +reader +virtual +device +established +answers +rent +las +remote +dark +programming +external +apple +le +regarding +instructions +min +offered +theory +enjoy +remove +aid +surface +minimum +visual +host +variety +teachers +isbn +martin +manual +block +subjects +agents +increased +repair +fair +civil +steel +understanding +songs +fixed +wrong +beginning +hands +associates +finally +az +updates +desktop +classes +paris +ohio +gets +sector +capacity +requires +jersey +un +fat +fully +father +electric +saw +instruments +quotes +officer +driver +businesses +dead +respect +unknown +specified +restaurant +mike +trip +pst +worth +mi +procedures +poor +teacher +eyes +relationship +workers +farm +georgia +peace +traditional +campus +tom +showing +creative +coast +benefit +progress +funding +devices +lord +grant +sub +agree +fiction +hear +sometimes +watches +careers +beyond +goes +families +led +museum +themselves +fan +transport +interesting +blogs +wife +evaluation +accepted +former +implementation +ten +hits +zone +complex +th +cat +galleries +references +die +presented +jack +flat +flow +agencies +literature +respective +parent +spanish +michigan +columbia +setting +dr +scale +stand +economy +highest +helpful +monthly +critical +frame +musical +definition +secretary +angeles +networking +path +australian +employee +chief +gives +kb +bottom +magazines +packages +detail +francisco +laws +changed +pet +heard +begin +individuals +colorado +royal +clean +switch +russian +largest +african +guy +titles +relevant +guidelines +justice +connect +bible +dev +cup +basket +applied +weekly +vol +installation +described +demand +pp +suite +vegas +na +square +chris +attention +advance +skip +diet +army +auction +gear +lee +os +difference +allowed +correct +charles +nation +selling +lots +piece +sheet +firm +seven +older +illinois +regulations +elements +species +jump +cells +module +resort +facility +random +pricing +dvds +certificate +minister +motion +looks +fashion +directions +visitors +documentation +monitor +trading +forest +calls +whose +coverage +couple +giving +chance +vision +ball +ending +clients +actions +listen +discuss +accept +automotive +naked +goal +successful +sold +wind +communities +clinical +situation +sciences +markets +lowest +highly +publishing +appear +emergency +developing +lives +currency +leather +determine +temperature +palm +announcements +patient +actual +historical +stone +bob +commerce +ringtones +perhaps +persons +difficult +scientific +satellite +fit +tests +village +accounts +amateur +ex +met +pain +xbox +particularly +factors +coffee +www +settings +buyer +cultural +steve +easily +oral +ford +poster +edge +functional +root +au +fi +closed +holidays +ice +pink +zealand +balance +monitoring +graduate +replies +shot +nc +architecture +initial +label +thinking +scott +llc +sec +recommend +canon +league +waste +minute +bus +provider +optional +dictionary +cold +accounting +manufacturing +sections +chair +fishing +effort +phase +fields +bag +fantasy +po +letters +motor +va +professor +context +install +shirt +apparel +generally +continued +foot +mass +crime +count +breast +techniques +ibm +rd +johnson +sc +quickly +dollars +websites +religion +claim +driving +permission +surgery +patch +heat +wild +measures +generation +kansas +miss +chemical +doctor +task +reduce +brought +himself +nor +component +enable +exercise +bug +santa +mid +guarantee +leader +diamond +israel +se +processes +soft +servers +alone +meetings +seconds +jones +arizona +keyword +interests +flight +congress +fuel +username +walk +produced +italian +paperback +classifieds +wait +supported +pocket +saint +rose +freedom +argument +competition +creating +jim +drugs +joint +premium +providers +fresh +characters +attorney +upgrade +di +factor +growing +thousands +km +stream +apartments +pick +hearing +eastern +auctions +therapy +entries +dates +generated +signed +upper +administrative +serious +prime +samsung +limit +began +louis +steps +errors +shops +del +efforts +informed +ga +ac +thoughts +creek +ft +worked +quantity +urban +practices +sorted +reporting +essential +myself +tours +platform +load +affiliate +labor +immediately +admin +nursing +defense +machines +designated +tags +heavy +covered +recovery +joe +guys +integrated +configuration +merchant +comprehensive +expert +universal +protect +drop +solid +cds +presentation +languages +became +orange +compliance +vehicles +prevent +theme +rich +im +campaign +marine +improvement +vs +guitar +finding +pennsylvania +examples +ipod +saying +spirit +ar +claims +challenge +motorola +acceptance +strategies +mo +seem +affairs +touch +intended +towards +sa +goals +hire +election +suggest +branch +charges +serve +affiliates +reasons +magic +mount +smart +talking +gave +ones +latin +multimedia +xp +avoid +certified +manage +corner +rank +computing +oregon +element +birth +virus +abuse +interactive +requests +separate +quarter +procedure +leadership +tables +define +racing +religious +facts +breakfast +kong +column +plants +faith +chain +developer +identify +avenue +missing +died +approximately +domestic +sitemap +recommendations +moved +houston +reach +comparison +mental +viewed +moment +extended +sequence +inch +attack +sorry +centers +opening +damage +lab +reserve +recipes +cvs +gamma +plastic +produce +snow +placed +truth +counter +failure +follows +eu +weekend +dollar +camp +ontario +automatically +des +minnesota +films +bridge +native +fill +williams +movement +printing +baseball +owned +approval +draft +chart +played +contacts +cc +jesus +readers +clubs +lcd +wa +jackson +equal +adventure +matching +offering +shirts +profit +leaders +posters +institutions +assistant +variable +ave +dj +advertisement +expect +parking +headlines +yesterday +compared +determined +wholesale +workshop +russia +gone +codes +kinds +extension +seattle +statements +golden +completely +teams +fort +cm +wi +lighting +senate +forces +funny +brother +gene +turned +portable +tried +electrical +applicable +disc +returned +pattern +ct +boat +named +theatre +laser +earlier +manufacturers +sponsor +classical +icon +warranty +dedicated +indiana +direction +harry +basketball +objects +ends +delete +evening +assembly +nuclear +taxes +mouse +signal +criminal +issued +brain +sexual +wisconsin +powerful +dream +obtained +false +da +cast +flower +felt +personnel +passed +supplied +identified +falls +pic +soul +aids +opinions +promote +stated +stats +hawaii +professionals +appears +carry +flag +decided +nj +covers +hr +em +advantage +hello +designs +maintain +tourism +priority +newsletters +adults +clips +savings +iv +graphic +atom +payments +rw +estimated +binding +brief +ended +winning +eight +anonymous +iron +straight +script +served +wants +miscellaneous +prepared +void +dining +alert +integration +atlanta +dakota +tag +interview +mix +framework +disk +installed +queen +vhs +credits +clearly +fix +handle +sweet +desk +criteria +pubmed +dave +massachusetts +diego +hong +vice +associate +ne +truck +behavior +enlarge +ray +frequently +revenue +measure +changing +votes +du +duty +looked +discussions +bear +gain +festival +laboratory +ocean +flights +experts +signs +lack +depth +iowa +whatever +logged +laptop +vintage +train +exactly +dry +explore +maryland +spa +concept +nearly +eligible +checkout +reality +forgot +handling +origin +knew +gaming +feeds +billion +destination +scotland +faster +intelligence +dallas +bought +con +ups +nations +route +followed +specifications +broken +tripadvisor +frank +alaska +zoom +blow +battle +residential +anime +speak +decisions +industries +protocol +query +clip +partnership +editorial +nt +expression +es +equity +provisions +speech +wire +principles +suggestions +rural +shared +sounds +replacement +tape +strategic +judge +spam +economics +acid +bytes +cent +forced +compatible +fight +apartment +height +null +zero +speaker +filed +gb +netherlands +obtain +bc +consulting +recreation +offices +designer +remain +managed +pr +failed +marriage +roll +korea +banks +fr +participants +secret +bath +aa +kelly +leads +negative +austin +favorites +toronto +theater +springs +missouri +andrew +var +perform +healthy +translation +estimates +font +assets +injury +mt +joseph +ministry +drivers +lawyer +figures +married +protected +proposal +sharing +philadelphia +portal +waiting +birthday +beta +fail +gratis +banking +officials +brian +toward +won +slightly +assist +conduct +contained +lingerie +legislation +calling +parameters +jazz +serving +bags +profiles +miami +comics +matters +houses +doc +postal +relationships +tennessee +wear +controls +breaking +combined +ultimate +wales +representative +frequency +introduced +minor +finish +departments +residents +noted +displayed +mom +reduced +physics +rare +spent +performed +extreme +samples +davis +daniel +bars +reviewed +row +oz +forecast +removed +helps +singles +administrator +cycle +amounts +contain +accuracy +dual +rise +usd +sleep +mg +bird +pharmacy +brazil +creation +static +scene +hunter +addresses +lady +crystal +famous +writer +chairman +violence +fans +oklahoma +speakers +drink +academy +dynamic +gender +eat +permanent +agriculture +dell +cleaning +constitutes +portfolio +practical +delivered +collectibles +infrastructure +exclusive +seat +concerns +vendor +originally +intel +utilities +philosophy +regulation +officers +reduction +aim +bids +referred +supports +nutrition +recording +regions +junior +toll +les +cape +ann +rings +meaning +tip +secondary +wonderful +mine +ladies +henry +ticket +announced +guess +agreed +prevention +whom +ski +soccer +math +import +posting +presence +instant +mentioned +automatic +healthcare +viewing +maintained +ch +increasing +majority +connected +christ +dan +dogs +sd +directors +aspects +austria +ahead +moon +participation +scheme +utility +preview +fly +manner +matrix +containing +combination +devel +amendment +despite +strength +guaranteed +turkey +libraries +proper +distributed +degrees +singapore +enterprises +delta +fear +seeking +inches +phoenix +rs +convention +shares +principal +daughter +standing +comfort +colors +wars +cisco +ordering +kept +alpha +appeal +cruise +bonus +certification +previously +hey +bookmark +buildings +specials +beat +disney +household +batteries +adobe +smoking +bbc +becomes +drives +arms +alabama +tea +improved +trees +avg +achieve +positions +dress +subscription +dealer +contemporary +sky +utah +nearby +rom +carried +happen +exposure +panasonic +hide +permalink +signature +gambling +refer +miller +provision +outdoors +clothes +caused +luxury +babes +frames +certainly +indeed +newspaper +toy +circuit +layer +printed +slow +removal +easier +src +liability +trademark +hip +printers +faqs +nine +adding +kentucky +mostly +eric +spot +taylor +trackback +prints +spend +factory +interior +revised +grow +americans +optical +promotion +relative +amazing +clock +dot +hiv +identity +suites +conversion +feeling +hidden +reasonable +victoria +serial +relief +revision +broadband +influence +ratio +pda +importance +rain +onto +dsl +planet +webmaster +copies +recipe +zum +permit +seeing +proof +dna +diff +tennis +bass +prescription +bedroom +empty +instance +hole +pets +ride +licensed +orlando +specifically +tim +bureau +maine +sql +represent +conservation +pair +ideal +specs +recorded +don +pieces +finished +parks +dinner +lawyers +sydney +stress +cream +ss +runs +trends +yeah +discover +ap +patterns +boxes +louisiana +hills +javascript +fourth +nm +advisor +mn +marketplace +nd +evil +aware +wilson +shape +evolution +irish +certificates +objectives +stations +suggested +gps +op +remains +acc +greatest +firms +concerned +euro +operator +structures +generic +encyclopedia +usage +cap +ink +charts +continuing +mixed +census +interracial +peak +tn +competitive +exist +wheel +transit +suppliers +salt +compact +poetry +lights +tracking +angel +bell +keeping +preparation +attempt +receiving +matches +accordance +width +noise +engines +forget +array +discussed +accurate +stephen +elizabeth +climate +reservations +pin +playstation +alcohol +greek +instruction +managing +annotation +sister +raw +differences +walking +explain +smaller +newest +establish +gnu +happened +expressed +jeff +extent +sharp +lesbians +ben +lane +paragraph +kill +mathematics +aol +compensation +ce +export +managers +aircraft +modules +sweden +conflict +conducted +versions +employer +occur +percentage +knows +mississippi +describe +concern +backup +requested +citizens +connecticut +heritage +personals +immediate +holding +trouble +spread +coach +kevin +agricultural +expand +supporting +audience +assigned +jordan +collections +ages +participate +plug +specialist +cook +affect +virgin +experienced +investigation +raised +hat +institution +directed +dealers +searching +sporting +helping +perl +affected +lib +bike +totally +plate +expenses +indicate +blonde +ab +proceedings +transmission +anderson +utc +characteristics +der +lose +organic +seek +experiences +albums +cheats +extremely +verzeichnis +contracts +guests +hosted +diseases +concerning +developers +equivalent +chemistry +tony +neighborhood +nevada +kits +thailand +variables +agenda +anyway +continues +tracks +advisory +cam +curriculum +logic +template +prince +circle +soil +grants +anywhere +psychology +responses +atlantic +wet +circumstances +edward +investor +identification +ram +leaving +wildlife +appliances +matt +elementary +cooking +speaking +sponsors +fox +unlimited +respond +sizes +plain +exit +entered +iran +arm +keys +launch +wave +checking +costa +belgium +printable +holy +acts +guidance +mesh +trail +enforcement +symbol +crafts +highway +buddy +hardcover +observed +dean +setup +poll +booking +glossary +fiscal +celebrity +styles +denver +unix +filled +bond +channels +ericsson +appendix +notify +blues +chocolate +pub +portion +scope +hampshire +supplier +cables +cotton +bluetooth +controlled +requirement +authorities +biology +dental +killed +border +ancient +debate +representatives +starts +pregnancy +causes +arkansas +biography +leisure +attractions +learned +transactions +notebook +explorer +historic +attached +opened +tm +husband +disabled +authorized +crazy +upcoming +britain +concert +retirement +scores +financing +efficiency +sp +comedy +adopted +efficient +weblog +linear +commitment +specialty +bears +jean +hop +carrier +edited +constant +visa +mouth +jewish +meter +linked +portland +interviews +concepts +nh +gun +reflect +pure +deliver +wonder +lessons +fruit +begins +qualified +reform +lens +alerts +treated +discovery +draw +mysql +classified +relating +assume +confidence +alliance +fm +confirm +warm +neither +lewis +howard +offline +leaves +engineer +lifestyle +consistent +replace +clearance +connections +inventory +converter +organisation +babe +checks +reached +becoming +safari +objective +indicated +sugar +crew +legs +sam +stick +securities +allen +pdt +relation +enabled +genre +slide +montana +volunteer +tested +rear +democratic +enhance +switzerland +exact +bound +parameter +adapter +processor +node +formal +dimensions +contribute +lock +hockey +storm +micro +colleges +laptops +mile +showed +challenges +editors +mens +threads +bowl +supreme +brothers +recognition +presents +ref +tank +submission +dolls +estimate +encourage +navy +kid +regulatory +inspection +consumers +cancel +limits +territory +transaction +manchester +weapons +paint +delay +pilot +outlet +contributions +continuous +db +czech +resulting +cambridge +initiative +novel +pan +execution +disability +increases +ultra +winner +idaho +contractor +ph +episode +examination +potter +dish +plays +bulletin +ia +pt +indicates +modify +oxford +adam +truly +epinions +painting +committed +extensive +affordable +universe +candidate +databases +patent +slot +psp +outstanding +ha +eating +perspective +planned +watching +lodge +messenger +mirror +tournament +consideration +ds +discounts +sterling +sessions +kernel +stocks +buyers +journals +gray +catalogue +ea +jennifer +antonio +charged +broad +taiwan +und +chosen +demo +greece +lg +swiss +sarah +clark +hate +terminal +publishers +nights +behalf +caribbean +liquid +rice +nebraska +loop +salary +reservation +foods +gourmet +guard +properly +orleans +saving +nfl +remaining +empire +resume +twenty +newly +raise +prepare +avatar +gary +depending +illegal +expansion +vary +hundreds +rome +arab +lincoln +helped +premier +tomorrow +purchased +milk +decide +consent +drama +visiting +performing +downtown +keyboard +contest +collected +nw +bands +boot +suitable +ff +absolutely +millions +lunch +audit +push +chamber +guinea +findings +muscle +featuring +iso +implement +clicking +scheduled +polls +typical +tower +yours +sum +misc +calculator +significantly +chicken +temporary +attend +shower +alan +sending +jason +tonight +dear +sufficient +holdem +shell +province +catholic +oak +vat +awareness +vancouver +governor +beer +seemed +contribution +measurement +swimming +spyware +formula +constitution +packaging +solar +jose +catch +jane +pakistan +ps +reliable +consultation +northwest +sir +doubt +earn +finder +unable +periods +classroom +tasks +democracy +attacks +kim +wallpaper +merchandise +const +resistance +doors +symptoms +resorts +biggest +memorial +visitor +twin +forth +insert +baltimore +gateway +ky +dont +alumni +drawing +candidates +charlotte +ordered +biological +fighting +transition +happens +preferences +spy +romance +instrument +bruce +split +themes +powers +heaven +br +bits +pregnant +twice +classification +focused +egypt +physician +hollywood +bargain +wikipedia +cellular +norway +vermont +asking +blocks +normally +lo +spiritual +hunting +diabetes +suit +ml +shift +chip +res +sit +bodies +photographs +cutting +wow +simon +writers +marks +flexible +loved +mapping +numerous +relatively +birds +satisfaction +represents +char +indexed +pittsburgh +superior +preferred +saved +paying +cartoon +shots +intellectual +moore +granted +choices +carbon +spending +comfortable +magnetic +interaction +listening +effectively +registry +crisis +outlook +massive +denmark +employed +bright +treat +header +cs +poverty +formed +piano +echo +que +grid +sheets +patrick +experimental +puerto +revolution +consolidation +displays +plasma +allowing +earnings +voip +mystery +landscape +dependent +mechanical +journey +delaware +bidding +consultants +risks +banner +applicant +charter +fig +barbara +cooperation +counties +acquisition +ports +implemented +sf +directories +recognized +dreams +blogger +notification +kg +licensing +stands +teach +occurred +textbooks +rapid +pull +hairy +diversity +cleveland +ut +reverse +deposit +seminar +investments +latina +nasa +wheels +specify +accessibility +dutch +sensitive +templates +formats +tab +depends +boots +holds +router +concrete +si +editing +poland +folder +womens +css +completion +upload +pulse +universities +technique +contractors +voting +courts +notices +subscriptions +calculate +mc +detroit +alexander +broadcast +converted +metro +toshiba +anniversary +improvements +strip +specification +pearl +accident +nick +accessible +accessory +resident +plot +qty +possibly +airline +typically +representation +regard +pump +exists +arrangements +smooth +conferences +uniprotkb +strike +consumption +birmingham +flashing +lp +narrow +afternoon +threat +surveys +sitting +putting +consultant +controller +ownership +committees +legislative +researchers +vietnam +trailer +anne +castle +gardens +missed +malaysia +unsubscribe +antique +labels +willing +bio +molecular +acting +heads +stored +exam +logos +residence +attorneys +antiques +density +hundred +ryan +operators +strange +sustainable +philippines +statistical +beds +mention +innovation +pcs +employers +grey +parallel +honda +amended +operate +bills +bold +bathroom +stable +opera +definitions +von +doctors +lesson +cinema +asset +ag +scan +elections +drinking +reaction +blank +enhanced +entitled +severe +generate +stainless +newspapers +hospitals +vi +deluxe +humor +aged +monitors +exception +lived +duration +bulk +successfully +indonesia +pursuant +sci +fabric +edt +visits +primarily +tight +domains +capabilities +pmid +contrast +recommendation +flying +recruitment +sin +berlin +cute +organized +ba +para +siemens +adoption +improving +cr +expensive +meant +capture +pounds +buffalo +organisations +plane +pg +explained +seed +programmes +desire +expertise +mechanism +camping +ee +jewellery +meets +welfare +peer +caught +eventually +marked +driven +measured +medline +bottle +agreements +considering +innovative +marshall +massage +rubber +conclusion +closing +tampa +thousand +meat +legend +grace +susan +ing +ks +adams +python +monster +alex +bang +villa +bone +columns +disorders +bugs +collaboration +hamilton +detection +ftp +cookies +inner +formation +tutorial +med +engineers +entity +cruises +gate +holder +proposals +moderator +sw +tutorials +settlement +portugal +lawrence +roman +duties +valuable +tone +collectables +ethics +forever +dragon +busy +captain +fantastic +imagine +brings +heating +leg +neck +hd +wing +governments +purchasing +scripts +abc +stereo +appointed +taste +dealing +commit +tiny +operational +rail +airlines +liberal +livecam +jay +trips +gap +sides +tube +turns +corresponding +descriptions +cache +belt +jacket +determination +animation +oracle +er +matthew +lease +productions +aviation +hobbies +proud +excess +disaster +console +commands +jr +telecommunications +instructor +giant +achieved +injuries +shipped +seats +approaches +biz +alarm +voltage +anthony +nintendo +usual +loading +stamps +appeared +franklin +angle +rob +vinyl +highlights +mining +designers +melbourne +ongoing +worst +imaging +betting +scientists +liberty +wyoming +blackjack +argentina +era +convert +possibility +analyst +commissioner +dangerous +garage +exciting +reliability +thongs +gcc +unfortunately +respectively +volunteers +attachment +ringtone +finland +morgan +derived +pleasure +honor +asp +oriented +eagle +desktops +pants +columbus +nurse +prayer +appointment +workshops +hurricane +quiet +luck +postage +producer +represented +mortgages +dial +responsibilities +cheese +comic +carefully +jet +productivity +investors +crown +par +underground +diagnosis +maker +crack +principle +picks +vacations +gang +semester +calculated +fetish +applies +casinos +appearance +smoke +apache +filters +incorporated +nv +craft +cake +notebooks +apart +fellow +blind +lounge +mad +algorithm +semi +coins +andy +gross +strongly +cafe +valentine +hilton +ken +proteins +horror +su +exp +familiar +capable +douglas +debian +till +involving +pen +investing +christopher +admission +epson +shoe +elected +carrying +victory +sand +madison +terrorism +joy +editions +cpu +mainly +ethnic +ran +parliament +actor +finds +seal +situations +fifth +allocated +citizen +vertical +corrections +structural +municipal +describes +prize +sr +occurs +jon +absolute +disabilities +consists +anytime +substance +prohibited +addressed +lies +pipe +soldiers +nr +guardian +lecture +simulation +layout +initiatives +ill +concentration +classics +lbs +lay +interpretation +horses +lol +dirty +deck +wayne +donate +taught +bankruptcy +mp +worker +optimization +alive +temple +substances +prove +discovered +wings +breaks +genetic +restrictions +participating +waters +promise +thin +exhibition +prefer +ridge +cabinet +modem +harris +mph +bringing +sick +dose +evaluate +tiffany +tropical +collect +bet +composition +toyota +streets +nationwide +vector +definitely +shaved +turning +buffer +purple +existence +commentary +larry +limousines +developments +def +immigration +destinations +lets +mutual +pipeline +necessarily +syntax +li +attribute +prison +skill +chairs +nl +everyday +apparently +surrounding +mountains +moves +popularity +inquiry +ethernet +checked +exhibit +throw +trend +sierra +visible +cats +desert +postposted +ya +oldest +rhode +nba +coordinator +obviously +mercury +steven +handbook +greg +navigate +worse +summit +victims +epa +spaces +fundamental +burning +escape +coupons +somewhat +receiver +substantial +tr +progressive +cialis +bb +boats +glance +scottish +championship +arcade +richmond +sacramento +impossible +ron +russell +tells +obvious +fiber +depression +graph +covering +platinum +judgment +bedrooms +talks +filing +foster +modeling +passing +awarded +testimonials +trials +tissue +nz +memorabilia +clinton +masters +bonds +cartridge +alberta +explanation +folk +org +commons +cincinnati +subsection +fraud +electricity +permitted +spectrum +arrival +okay +pottery +emphasis +roger +aspect +workplace +awesome +mexican +confirmed +counts +priced +wallpapers +hist +crash +lift +desired +inter +closer +assumes +heights +shadow +riding +infection +firefox +lisa +expense +grove +eligibility +venture +clinic +korean +healing +princess +mall +entering +packet +spray +studios +involvement +dad +buttons +placement +observations +vbulletin +funded +thompson +winners +extend +roads +subsequent +pat +dublin +rolling +fell +motorcycle +yard +disclosure +establishment +memories +nelson +te +arrived +creates +faces +tourist +av +mayor +murder +sean +adequate +senator +yield +presentations +grades +cartoons +pour +digest +reg +lodging +tion +dust +hence +wiki +entirely +replaced +radar +rescue +undergraduate +losses +combat +reducing +stopped +occupation +lakes +donations +associations +citysearch +closely +radiation +diary +seriously +kings +shooting +kent +adds +nsw +ear +flags +pci +baker +launched +elsewhere +pollution +conservative +guestbook +shock +effectiveness +walls +abroad +ebony +tie +ward +drawn +arthur +ian +visited +roof +walker +demonstrate +atmosphere +suggests +kiss +beast +ra +operated +experiment +targets +overseas +purchases +dodge +counsel +federation +pizza +invited +yards +assignment +chemicals +gordon +mod +farmers +rc +queries +bmw +rush +ukraine +absence +nearest +cluster +vendors +mpeg +whereas +yoga +serves +woods +surprise +lamp +rico +partial +shoppers +phil +everybody +couples +nashville +ranking +jokes +cst +http +ceo +simpson +twiki +sublime +counseling +palace +acceptable +satisfied +glad +wins +measurements +verify +globe +trusted +copper +milwaukee +rack +medication +warehouse +shareware +ec +rep +dicke +kerry +receipt +supposed +ordinary +nobody +ghost +violation +configure +stability +mit +applying +southwest +boss +pride +institutional +expectations +independence +knowing +reporter +metabolism +keith +champion +cloudy +linda +ross +personally +chile +anna +plenty +solo +sentence +throat +ignore +maria +uniform +excellence +wealth +tall +rm +somewhere +vacuum +dancing +attributes +recognize +brass +writes +plaza +pdas +outcomes +survival +quest +publish +sri +screening +toe +thumbnail +trans +jonathan +whenever +nova +lifetime +api +pioneer +booty +forgotten +acrobat +plates +acres +venue +athletic +thermal +essays +vital +telling +fairly +coastal +config +cf +charity +intelligent +edinburgh +vt +excel +modes +obligation +campbell +wake +stupid +harbor +hungary +traveler +urw +segment +realize +regardless +lan +enemy +puzzle +rising +aluminum +wells +wishlist +opens +insight +sms +restricted +republican +secrets +lucky +latter +merchants +thick +trailers +repeat +syndrome +philips +attendance +penalty +drum +glasses +enables +nec +iraqi +builder +vista +jessica +chips +terry +flood +foto +ease +arguments +amsterdam +arena +adventures +pupils +stewart +announcement +tabs +outcome +appreciate +expanded +casual +grown +polish +lovely +extras +gm +centres +jerry +clause +smile +lands +ri +troops +indoor +bulgaria +armed +broker +charger +regularly +believed +pine +cooling +tend +gulf +rt +rick +trucks +cp +mechanisms +divorce +laura +shopper +tokyo +partly +nikon +customize +tradition +candy +pills +tiger +donald +folks +sensor +exposed +telecom +hunt +angels +deputy +indicators +sealed +thai +emissions +physicians +loaded +fred +complaint +scenes +experiments +afghanistan +dd +boost +spanking +scholarship +governance +mill +founded +supplements +chronic +icons +moral +den +catering +aud +finger +keeps +pound +locate +camcorder +pl +trained +burn +implementing +roses +labs +ourselves +bread +tobacco +wooden +motors +tough +roberts +incident +gonna +dynamics +lie +crm +rf +conversation +decrease +chest +pension +billy +revenues +emerging +worship +capability +ak +fe +craig +herself +producing +churches +precision +damages +reserves +contributed +solve +shorts +reproduction +minority +td +diverse +amp +ingredients +sb +ah +johnny +sole +franchise +recorder +complaints +facing +sm +nancy +promotions +tones +passion +rehabilitation +maintaining +sight +laid +clay +defence +patches +weak +refund +usc +towns +environments +trembl +divided +blvd +reception +amd +wise +emails +cyprus +wv +odds +correctly +insider +seminars +consequences +makers +hearts +geography +appearing +integrity +worry +ns +discrimination +eve +carter +legacy +marc +pleased +danger +vitamin +widely +processed +phrase +genuine +raising +implications +functionality +paradise +hybrid +reads +roles +intermediate +emotional +sons +leaf +pad +glory +platforms +ja +bigger +billing +diesel +versus +combine +overnight +geographic +exceed +bs +rod +saudi +fault +cuba +hrs +preliminary +districts +introduce +silk +promotional +kate +chevrolet +babies +bi +karen +compiled +romantic +revealed +specialists +generator +albert +examine +jimmy +graham +suspension +bristol +margaret +compaq +sad +correction +wolf +slowly +authentication +communicate +rugby +supplement +showtimes +cal +portions +infant +promoting +sectors +samuel +fluid +grounds +fits +kick +regards +meal +ta +hurt +machinery +bandwidth +unlike +equation +baskets +probability +pot +dimension +wright +img +barry +proven +schedules +admissions +cached +warren +slip +studied +reviewer +involves +quarterly +rpm +profits +devil +grass +comply +marie +florist +illustrated +cherry +continental +alternate +deutsch +achievement +limitations +kenya +webcam +cuts +funeral +nutten +earrings +enjoyed +automated +chapters +pee +charlie +quebec +passenger +convenient +dennis +mars +francis +tvs +sized +manga +noticed +socket +silent +literary +egg +mhz +signals +caps +orientation +pill +theft +childhood +swing +symbols +lat +meta +humans +analog +facial +choosing +talent +dated +flexibility +seeker +wisdom +shoot +boundary +mint +packard +offset +payday +philip +elite +gi +spin +holders +believes +swedish +poems +deadline +jurisdiction +robot +displaying +witness +collins +equipped +stages +encouraged +sur +winds +powder +broadway +acquired +assess +wash +cartridges +stones +entrance +gnome +roots +declaration +losing +attempts +gadgets +noble +glasgow +automation +impacts +rev +gospel +advantages +shore +loves +induced +ll +knight +preparing +loose +aims +recipient +linking +extensions +appeals +cl +earned +illness +islamic +athletics +southeast +ieee +ho +alternatives +pending +parker +determining +lebanon +corp +personalized +kennedy +gt +sh +conditioning +teenage +soap +ae +triple +cooper +nyc +vincent +jam +secured +unusual +answered +partnerships +destruction +slots +increasingly +migration +disorder +routine +toolbar +basically +rocks +conventional +titans +applicants +wearing +axis +sought +genes +mounted +habitat +firewall +median +guns +scanner +herein +occupational +animated +judicial +rio +hs +adjustment +hero +integer +treatments +bachelor +attitude +camcorders +engaged +falling +basics +montreal +carpet +rv +struct +lenses +binary +genetics +attended +difficulty +punk +collective +coalition +pi +dropped +enrollment +duke +walter +ai +pace +besides +wage +producers +ot +collector +arc +hosts +interfaces +advertisers +moments +atlas +strings +dawn +representing +observation +feels +torture +carl +deleted +coat +mitchell +mrs +rica +restoration +convenience +returning +ralph +opposition +container +yr +defendant +warner +confirmation +app +embedded +inkjet +supervisor +wizard +corps +actors +liver +peripherals +liable +brochure +morris +bestsellers +petition +eminem +recall +antenna +picked +assumed +departure +minneapolis +belief +killing +bikini +memphis +shoulder +decor +lookup +texts +harvard +brokers +roy +ion +diameter +ottawa +doll +ic +podcast +seasons +peru +interactions +refine +bidder +singer +evans +herald +literacy +fails +aging +nike +intervention +fed +plugin +attraction +diving +invite +modification +alice +latinas +suppose +customized +reed +involve +moderate +terror +younger +thirty +mice +opposite +understood +rapidly +dealtime +ban +temp +intro +mercedes +zus +assurance +clerk +happening +vast +mills +outline +amendments +tramadol +holland +receives +jeans +metropolitan +compilation +verification +fonts +ent +odd +wrap +refers +mood +favor +veterans +quiz +mx +sigma +gr +attractive +xhtml +occasion +recordings +jefferson +victim +demands +sleeping +careful +ext +beam +gardening +obligations +arrive +orchestra +sunset +tracked +moreover +minimal +polyphonic +lottery +tops +framed +aside +outsourcing +licence +adjustable +allocation +michelle +essay +discipline +amy +ts +demonstrated +dialogue +identifying +alphabetical +camps +declared +dispatched +aaron +handheld +trace +disposal +shut +florists +packs +ge +installing +switches +romania +voluntary +ncaa +thou +consult +phd +greatly +blogging +mask +cycling +midnight +ng +commonly +pe +photographer +inform +turkish +coal +cry +messaging +pentium +quantum +murray +intent +tt +zoo +largely +pleasant +announce +constructed +additions +requiring +spoke +aka +arrow +engagement +sampling +rough +weird +tee +refinance +lion +inspired +holes +weddings +blade +suddenly +oxygen +cookie +meals +canyon +goto +meters +merely +calendars +arrangement +conclusions +passes +bibliography +pointer +compatibility +stretch +durham +furthermore +permits +cooperative +muslim +xl +neil +sleeve +netscape +cleaner +cricket +beef +feeding +stroke +township +rankings +measuring +cad +hats +robin +robinson +jacksonville +strap +headquarters +sharon +crowd +tcp +transfers +surf +olympic +transformation +remained +attachments +dv +dir +entities +customs +administrators +personality +rainbow +hook +roulette +decline +gloves +israeli +medicare +cord +skiing +cloud +facilitate +subscriber +valve +val +hewlett +explains +proceed +flickr +feelings +knife +jamaica +priorities +shelf +bookstore +timing +liked +parenting +adopt +denied +fotos +incredible +britney +freeware +donation +outer +crop +deaths +rivers +commonwealth +pharmaceutical +manhattan +tales +katrina +workforce +islam +nodes +tu +fy +thumbs +seeds +cited +lite +ghz +hub +targeted +organizational +skype +realized +twelve +founder +decade +gamecube +rr +dispute +portuguese +tired +titten +adverse +everywhere +excerpt +eng +steam +discharge +ef +drinks +ace +voices +acute +halloween +climbing +stood +sing +tons +perfume +carol +honest +albany +hazardous +restore +stack +methodology +somebody +sue +ep +housewares +reputation +resistant +democrats +recycling +hang +gbp +curve +creator +amber +qualifications +museums +coding +slideshow +tracker +variation +passage +transferred +trunk +hiking +lb +pierre +jelsoft +headset +photograph +oakland +colombia +waves +camel +distributor +lamps +underlying +hood +wrestling +suicide +archived +photoshop +jp +chi +bt +arabia +gathering +projection +juice +chase +mathematical +logical +sauce +fame +extract +specialized +diagnostic +panama +indianapolis +af +payable +corporations +courtesy +criticism +automobile +confidential +rfc +statutory +accommodations +athens +northeast +downloaded +judges +sl +seo +retired +isp +remarks +detected +decades +paintings +walked +arising +nissan +bracelet +ins +eggs +juvenile +injection +yorkshire +populations +protective +afraid +acoustic +railway +cassette +initially +indicator +pointed +hb +jpg +causing +mistake +norton +locked +eliminate +tc +fusion +mineral +sunglasses +ruby +steering +beads +fortune +preference +canvas +threshold +parish +claimed +screens +cemetery +planner +croatia +flows +stadium +venezuela +exploration +mins +fewer +sequences +coupon +nurses +ssl +stem +proxy +astronomy +lanka +opt +edwards +drew +contests +flu +translate +announces +mlb +costume +tagged +berkeley +voted +killer +bikes +gates +adjusted +rap +tune +bishop +pulled +corn +gp +shaped +compression +seasonal +establishing +farmer +counters +puts +constitutional +grew +perfectly +tin +slave +instantly +cultures +norfolk +coaching +examined +trek +encoding +litigation +submissions +oem +heroes +painted +lycos +ir +zdnet +broadcasting +horizontal +artwork +cosmetic +resulted +portrait +terrorist +informational +ethical +carriers +ecommerce +mobility +floral +builders +ties +struggle +schemes +suffering +neutral +fisher +rat +spears +prospective +bedding +ultimately +joining +heading +equally +artificial +bearing +spectacular +coordination +connector +brad +combo +seniors +worlds +guilty +affiliated +activation +naturally +haven +tablet +jury +dos +tail +subscribers +charm +lawn +violent +mitsubishi +underwear +basin +soup +potentially +ranch +constraints +crossing +inclusive +dimensional +cottage +drunk +considerable +crimes +resolved +mozilla +byte +toner +nose +latex +branches +anymore +oclc +delhi +holdings +alien +locator +selecting +processors +pantyhose +plc +broke +nepal +zimbabwe +difficulties +juan +complexity +msg +constantly +browsing +resolve +barcelona +presidential +documentary +cod +territories +melissa +moscow +thesis +thru +jews +nylon +palestinian +discs +rocky +bargains +frequent +trim +nigeria +ceiling +pixels +ensuring +hispanic +cv +cb +legislature +hospitality +gen +anybody +procurement +diamonds +espn +fleet +untitled +bunch +totals +marriott +singing +theoretical +afford +exercises +starring +referral +nhl +surveillance +optimal +quit +distinct +protocols +lung +highlight +substitute +inclusion +hopefully +brilliant +turner +sucking +cents +reuters +ti +fc +gel +todd +spoken +omega +evaluated +stayed +civic +assignments +fw +manuals +doug +sees +termination +watched +saver +thereof +grill +households +gs +redeem +rogers +grain +aaa +authentic +regime +wanna +wishes +bull +montgomery +architectural +louisville +depend +differ +macintosh +movements +ranging +monica +repairs +breath +amenities +virtually +cole +mart +candle +hanging +colored +authorization +tale +verified +lynn +formerly +projector +bp +situated +comparative +std +seeks +herbal +loving +strictly +routing +docs +stanley +psychological +surprised +retailer +vitamins +elegant +gains +renewal +vid +genealogy +opposed +deemed +scoring +expenditure +brooklyn +liverpool +sisters +critics +connectivity +spots +oo +algorithms +hacker +madrid +similarly +margin +coin +solely +fake +salon +collaborative +norman +fda +excluding +turbo +headed +voters +cure +madonna +commander +arch +ni +murphy +thinks +thats +suggestion +hdtv +soldier +phillips +asin +aimed +justin +bomb +harm +interval +mirrors +spotlight +tricks +reset +brush +investigate +thy +expansys +panels +repeated +assault +connecting +spare +logistics +deer +kodak +tongue +bowling +tri +danish +pal +monkey +proportion +filename +skirt +florence +invest +honey +um +analyzes +drawings +significance +scenario +ye +fs +lovers +atomic +approx +symposium +arabic +gauge +essentials +junction +protecting +nn +faced +mat +rachel +solving +transmitted +weekends +screenshots +produces +oven +ted +intensive +chains +kingston +sixth +engage +deviant +noon +switching +quoted +adapters +correspondence +farms +imports +supervision +cheat +bronze +expenditures +sandy +separation +testimony +suspect +celebrities +macro +sender +mandatory +boundaries +crucial +syndication +gym +celebration +kde +adjacent +filtering +tuition +spouse +exotic +viewer +signup +threats +luxembourg +puzzles +reaching +vb +damaged +cams +receptor +laugh +joel +surgical +destroy +citation +pitch +autos +yo +premises +perry +proved +offensive +imperial +dozen +benjamin +deployment +teeth +cloth +studying +colleagues +stamp +lotus +salmon +olympus +separated +proc +cargo +tan +directive +fx +salem +mate +dl +starter +upgrades +likes +butter +pepper +weapon +luggage +burden +chef +tapes +zones +races +isle +stylish +slim +maple +luke +grocery +offshore +governing +retailers +depot +kenneth +comp +alt +pie +blend +harrison +ls +julie +occasionally +cbs +attending +emission +pete +spec +finest +realty +janet +bow +penn +recruiting +apparent +instructional +phpbb +autumn +traveling +probe +midi +permissions +biotechnology +toilet +ranked +jackets +routes +packed +excited +outreach +helen +mounting +recover +tied +lopez +balanced +prescribed +catherine +timely +talked +debug +delayed +chuck +reproduced +hon +dale +explicit +calculation +villas +ebook +consolidated +exclude +peeing +occasions +brooks +equations +newton +oils +sept +exceptional +anxiety +bingo +whilst +spatial +respondents +unto +lt +ceramic +prompt +precious +minds +annually +considerations +scanners +atm +xanax +eq +pays +fingers +sunny +ebooks +delivers +je +queensland +necklace +musicians +leeds +composite +unavailable +cedar +arranged +lang +theaters +advocacy +raleigh +stud +fold +essentially +designing +threaded +uv +qualify +blair +hopes +assessments +cms +mason +diagram +burns +pumps +footwear +sg +vic +beijing +peoples +victor +mario +pos +attach +licenses +utils +removing +advised +brunswick +spider +phys +ranges +pairs +sensitivity +trails +preservation +hudson +isolated +calgary +interim +assisted +divine +streaming +approve +chose +compound +intensity +technological +syndicate +abortion +dialog +venues +blast +wellness +calcium +newport +antivirus +addressing +pole +discounted +indians +shield +harvest +membrane +prague +previews +bangladesh +constitute +locally +concluded +pickup +desperate +mothers +nascar +iceland +demonstration +governmental +manufactured +candles +graduation +mega +bend +sailing +variations +moms +sacred +addiction +morocco +chrome +tommy +springfield +refused +brake +exterior +greeting +ecology +oliver +congo +glen +botswana +nav +delays +synthesis +olive +undefined +unemployment +cyber +verizon +scored +enhancement +newcastle +clone +velocity +lambda +relay +composed +tears +performances +oasis +baseline +cab +angry +fa +societies +silicon +brazilian +identical +petroleum +compete +ist +norwegian +lover +belong +honolulu +beatles +lips +retention +exchanges +pond +rolls +thomson +barnes +soundtrack +wondering +malta +daddy +lc +ferry +rabbit +profession +seating +dam +cnn +separately +physiology +lil +collecting +das +exports +omaha +tire +participant +scholarships +recreational +dominican +chad +electron +loads +friendship +heather +passport +motel +unions +treasury +warrant +sys +solaris +frozen +occupied +josh +royalty +scales +rally +observer +sunshine +strain +drag +ceremony +somehow +arrested +expanding +provincial +investigations +icq +ripe +yamaha +rely +medications +hebrew +gained +rochester +dying +laundry +stuck +solomon +placing +stops +homework +adjust +assessed +advertiser +enabling +encryption +filling +downloadable +sophisticated +imposed +silence +scsi +focuses +soviet +possession +cu +laboratories +treaty +vocal +trainer +organ +stronger +volumes +advances +vegetables +lemon +toxic +dns +thumbnails +darkness +pty +ws +nuts +nail +bizrate +vienna +implied +span +stanford +sox +stockings +joke +respondent +packing +statute +rejected +satisfy +destroyed +shelter +chapel +gamespot +manufacture +layers +wordpress +guided +vulnerability +accountability +celebrate +accredited +appliance +compressed +bahamas +powell +mixture +bench +univ +tub +rider +scheduling +radius +perspectives +mortality +logging +hampton +christians +borders +therapeutic +pads +butts +inns +bobby +impressive +sheep +accordingly +architect +railroad +lectures +challenging +wines +nursery +harder +cups +ash +microwave +cheapest +accidents +travesti +relocation +stuart +contributors +salvador +ali +salad +np +monroe +tender +violations +foam +temperatures +paste +clouds +competitions +discretion +tft +tanzania +preserve +jvc +poem +unsigned +staying +cosmetics +easter +theories +repository +praise +jeremy +venice +concentrations +estonia +christianity +veteran +streams +landing +signing +executed +katie +negotiations +realistic +dt +cgi +showcase +integral +asks +relax +namibia +generating +christina +congressional +synopsis +hardly +prairie +reunion +composer +bean +sword +absent +photographic +sells +ecuador +hoping +accessed +spirits +modifications +coral +pixel +float +colin +bias +imported +paths +bubble +por +acquire +contrary +millennium +tribune +vessel +acids +focusing +viruses +cheaper +admitted +dairy +admit +mem +fancy +equality +samoa +gc +achieving +tap +stickers +fisheries +exceptions +reactions +leasing +lauren +beliefs +ci +macromedia +companion +squad +analyze +ashley +scroll +relate +divisions +swim +wages +additionally +suffer +forests +fellowship +nano +invalid +concerts +martial +males +victorian +retain +execute +tunnel +genres +cambodia +patents +copyrights +yn +chaos +lithuania +mastercard +wheat +chronicles +obtaining +beaver +updating +distribute +readings +decorative +kijiji +confused +compiler +enlargement +eagles +bases +vii +accused +bee +campaigns +unity +loud +conjunction +bride +rats +defines +airports +instances +indigenous +begun +cfr +brunette +packets +anchor +socks +validation +parade +corruption +stat +trigger +incentives +cholesterol +gathered +essex +slovenia +notified +differential +beaches +folders +dramatic +surfaces +terrible +routers +cruz +pendant +dresses +baptist +scientist +starsmerchant +hiring +clocks +arthritis +bios +females +wallace +nevertheless +reflects +taxation +fever +pmc +cuisine +surely +practitioners +transcript +myspace +theorem +inflation +thee +nb +ruth +pray +stylus +compounds +pope +drums +contracting +arnold +structured +reasonably +jeep +chicks +bare +hung +cattle +mba +radical +graduates +rover +recommends +controlling +treasure +reload +distributors +flame +levitra +tanks +assuming +monetary +elderly +pit +arlington +mono +particles +floating +extraordinary +tile +indicating +bolivia +spell +hottest +stevens +coordinate +kuwait +exclusively +emily +alleged +limitation +widescreen +compile +webster +struck +rx +illustration +plymouth +warnings +construct +apps +inquiries +bridal +annex +mag +gsm +inspiration +tribal +curious +affecting +freight +rebate +meetup +eclipse +sudan +ddr +downloading +rec +shuttle +aggregate +stunning +cycles +affects +forecasts +detect +actively +ciao +ampland +knee +prep +pb +complicated +chem +fastest +butler +shopzilla +injured +decorating +payroll +cookbook +expressions +ton +courier +uploaded +shakespeare +hints +collapse +americas +connectors +unlikely +oe +gif +pros +conflicts +techno +beverage +tribute +wired +elvis +immune +latvia +travelers +forestry +barriers +cant +jd +rarely +gpl +infected +offerings +martha +genesis +barrier +argue +incorrect +trains +metals +bicycle +furnishings +letting +arise +guatemala +celtic +thereby +irc +jamie +particle +perception +minerals +advise +humidity +bottles +boxing +wy +dm +bangkok +renaissance +pathology +sara +bra +ordinance +hughes +photographers +infections +jeffrey +chess +operates +brisbane +configured +survive +oscar +festivals +menus +joan +possibilities +duck +reveal +canal +amino +phi +contributing +herbs +clinics +mls +cow +manitoba +analytical +missions +watson +lying +costumes +strict +dive +saddam +circulation +drill +offense +bryan +cet +protest +assumption +jerusalem +hobby +tries +transexuales +invention +nickname +fiji +technician +inline +executives +enquiries +washing +audi +staffing +cognitive +exploring +trick +enquiry +closure +raid +ppc +timber +volt +intense +div +playlist +registrar +showers +supporters +ruling +steady +dirt +statutes +withdrawal +myers +drops +predicted +wider +saskatchewan +jc +cancellation +plugins +enrolled +sensors +screw +ministers +publicly +hourly +blame +geneva +freebsd +veterinary +acer +prostores +reseller +dist +handed +suffered +intake +informal +relevance +incentive +butterfly +tucson +mechanics +heavily +swingers +fifty +headers +mistakes +numerical +ons +geek +uncle +defining +counting +reflection +sink +accompanied +assure +invitation +devoted +princeton +jacob +sodium +randy +spirituality +hormone +meanwhile +proprietary +timothy +childrens +brick +grip +naval +thumbzilla +medieval +porcelain +avi +bridges +pichunter +captured +watt +thehun +decent +casting +dayton +translated +shortly +cameron +columnists +pins +carlos +reno +donna +andreas +warrior +diploma +cabin +innocent +scanning +ide +consensus +polo +valium +copying +rpg +delivering +cordless +patricia +horn +eddie +uganda +fired +journalism +pd +prot +trivia +adidas +perth +frog +grammar +intention +syria +disagree +klein +harvey +tires +logs +undertaken +tgp +hazard +retro +leo +statewide +semiconductor +gregory +episodes +boolean +circular +anger +diy +mainland +illustrations +suits +chances +interact +snap +happiness +arg +substantially +bizarre +glenn +ur +auckland +olympics +fruits +identifier +geo +ribbon +calculations +doe +jpeg +conducting +startup +suzuki +trinidad +ati +kissing +wal +handy +swap +exempt +crops +reduces +accomplished +calculators +geometry +impression +abs +slovakia +flip +guild +correlation +gorgeous +capitol +sim +dishes +rna +barbados +chrysler +nervous +refuse +extends +fragrance +mcdonald +replica +plumbing +brussels +tribe +neighbors +trades +superb +buzz +transparent +nuke +rid +trinity +charleston +handled +legends +boom +calm +champions +floors +selections +projectors +inappropriate +exhaust +comparing +shanghai +speaks +burton +vocational +davidson +copied +scotia +farming +gibson +pharmacies +fork +troy +ln +roller +introducing +batch +organize +appreciated +alter +nicole +latino +ghana +edges +uc +mixing +handles +skilled +fitted +albuquerque +harmony +distinguished +asthma +projected +assumptions +shareholders +twins +developmental +rip +zope +regulated +triangle +amend +anticipated +oriental +reward +windsor +zambia +completing +gmbh +buf +ld +hydrogen +webshots +sprint +comparable +chick +advocate +sims +confusion +copyrighted +tray +inputs +warranties +genome +escorts +documented +thong +medal +paperbacks +coaches +vessels +walks +sol +keyboards +sage +knives +eco +vulnerable +arrange +artistic +bat +honors +booth +indie +reflected +unified +bones +breed +detector +ignored +polar +fallen +precise +sussex +respiratory +notifications +msgid +transexual +mainstream +invoice +evaluating +lip +subcommittee +sap +gather +suse +maternity +backed +alfred +colonial +mf +carey +motels +forming +embassy +cave +journalists +danny +rebecca +slight +proceeds +indirect +amongst +wool +foundations +msgstr +arrest +volleyball +mw +adipex +horizon +nu +deeply +toolbox +ict +marina +liabilities +prizes +bosnia +browsers +decreased +patio +dp +tolerance +surfing +creativity +lloyd +describing +optics +pursue +lightning +overcome +eyed +ou +quotations +grab +inspector +attract +brighton +beans +bookmarks +ellis +disable +snake +succeed +leonard +lending +oops +reminder +xi +searched +behavioral +riverside +bathrooms +plains +sku +ht +raymond +insights +abilities +initiated +sullivan +za +midwest +karaoke +trap +lonely +fool +ve +nonprofit +lancaster +suspended +hereby +observe +julia +containers +attitudes +karl +berry +collar +simultaneously +racial +integrate +bermuda +amanda +sociology +mobiles +screenshot +exhibitions +kelkoo +confident +retrieved +exhibits +officially +consortium +dies +terrace +bacteria +pts +replied +seafood +novels +rh +rrp +recipients +ought +delicious +traditions +fg +jail +safely +finite +kidney +periodically +fixes +sends +durable +mazda +allied +throws +moisture +hungarian +roster +referring +symantec +spencer +wichita +nasdaq +uruguay +ooo +hz +transform +timer +tablets +tuning +gotten +educators +tyler +futures +vegetable +verse +highs +humanities +independently +wanting +custody +scratch +launches +ipaq +alignment +henderson +bk +britannica +comm +ellen +competitors +nhs +rocket +aye +bullet +towers +racks +lace +nasty +visibility +latitude +consciousness +ste +tumor +ugly +deposits +beverly +mistress +encounter +trustees +watts +duncan +reprints +hart +bernard +resolutions +ment +accessing +forty +tubes +attempted +col +midlands +priest +floyd +ronald +analysts +queue +dx +sk +trance +locale +nicholas +biol +yu +bundle +hammer +invasion +witnesses +runner +rows +administered +notion +sq +skins +mailed +oc +fujitsu +spelling +arctic +exams +rewards +beneath +strengthen +defend +aj +frederick +medicaid +treo +infrared +seventh +gods +une +welsh +belly +aggressive +tex +advertisements +quarters +stolen +cia +sublimedirectory +soonest +haiti +disturbed +determines +sculpture +poly +ears +dod +wp +fist +naturals +neo +motivation +lenders +pharmacology +fitting +fixtures +bloggers +mere +agrees +passengers +quantities +petersburg +consistently +powerpoint +cons +surplus +elder +sonic +obituaries +cheers +dig +taxi +punishment +appreciation +subsequently +om +belarus +nat +zoning +gravity +providence +thumb +restriction +incorporate +backgrounds +treasurer +guitars +essence +flooring +lightweight +ethiopia +tp +mighty +athletes +humanity +transcription +jm +holmes +complications +scholars +dpi +scripting +gis +remembered +galaxy +chester +snapshot +caring +loc +worn +synthetic +shaw +vp +segments +testament +expo +dominant +twist +specifics +itunes +stomach +partially +buried +cn +newbie +minimize +darwin +ranks +wilderness +debut +generations +tournaments +bradley +deny +anatomy +bali +judy +sponsorship +headphones +fraction +trio +proceeding +cube +defects +volkswagen +uncertainty +breakdown +milton +marker +reconstruction +subsidiary +strengths +clarity +rugs +sandra +adelaide +encouraging +furnished +monaco +settled +folding +emirates +terrorists +airfare +comparisons +beneficial +distributions +vaccine +belize +fate +viewpicture +promised +volvo +penny +robust +bookings +threatened +minolta +republicans +discusses +gui +porter +gras +jungle +ver +rn +responded +rim +abstracts +zen +ivory +alpine +dis +prediction +pharmaceuticals +andale +fabulous +remix +alias +thesaurus +individually +battlefield +literally +newer +kay +ecological +spice +oval +implies +cg +soma +ser +cooler +appraisal +consisting +maritime +periodic +submitting +overhead +ascii +prospect +shipment +breeding +citations +geographical +donor +mozambique +tension +href +benz +trash +shapes +wifi +tier +fwd +earl +manor +envelope +diane +homeland +disclaimers +championships +excluded +andrea +breeds +rapids +disco +sheffield +bailey +aus +endif +finishing +emotions +wellington +incoming +prospects +lexmark +cleaners +bulgarian +hwy +eternal +cashiers +guam +cite +aboriginal +remarkable +rotation +nam +preventing +productive +boulevard +eugene +ix +gdp +pig +metric +compliant +minus +penalties +bennett +imagination +hotmail +refurbished +joshua +armenia +varied +grande +closest +activated +actress +mess +conferencing +assign +armstrong +politicians +trackbacks +lit +accommodate +tigers +aurora +una +slides +milan +premiere +lender +villages +shade +chorus +christine +rhythm +digit +argued +dietary +symphony +clarke +sudden +accepting +precipitation +marilyn +lions +findlaw +ada +pools +tb +lyric +claire +isolation +speeds +sustained +matched +approximate +rope +carroll +rational +programmer +fighters +chambers +dump +greetings +inherited +warming +incomplete +vocals +chronicle +fountain +chubby +grave +legitimate +biographies +burner +yrs +foo +investigator +gba +plaintiff +finnish +gentle +bm +prisoners +deeper +muslims +hose +mediterranean +nightlife +footage +howto +worthy +reveals +architects +saints +entrepreneur +carries +sig +freelance +duo +excessive +devon +screensaver +helena +saves +regarded +valuation +unexpected +cigarette +fog +characteristic +marion +lobby +egyptian +tunisia +metallica +outlined +consequently +headline +treating +punch +appointments +str +gotta +cowboy +narrative +bahrain +enormous +karma +consist +betty +queens +academics +pubs +quantitative +lucas +screensavers +subdivision +tribes +vip +defeat +clicks +distinction +honduras +naughty +hazards +insured +harper +livestock +mardi +exemption +tenant +sustainability +cabinets +tattoo +shake +algebra +shadows +holly +formatting +silly +nutritional +yea +mercy +hartford +freely +marcus +sunrise +wrapping +mild +fur +nicaragua +weblogs +timeline +tar +belongs +rj +readily +affiliation +soc +fence +nudist +infinite +diana +ensures +relatives +lindsay +clan +legally +shame +satisfactory +revolutionary +bracelets +sync +civilian +telephony +mesa +fatal +remedy +realtors +breathing +briefly +thickness +adjustments +graphical +genius +discussing +aerospace +fighter +meaningful +flesh +retreat +adapted +barely +wherever +estates +rug +democrat +borough +maintains +failing +shortcuts +ka +retained +voyeurweb +pamela +andrews +marble +extending +jesse +specifies +hull +logitech +surrey +briefing +belkin +dem +accreditation +wav +blackberry +highland +meditation +modular +microphone +macedonia +combining +brandon +instrumental +giants +organizing +shed +balloon +moderators +winston +memo +ham +solved +tide +kazakhstan +hawaiian +standings +partition +invisible +gratuit +consoles +funk +fbi +qatar +magnet +translations +porsche +cayman +jaguar +reel +sheer +commodity +posing +kilometers +rp +bind +thanksgiving +rand +hopkins +urgent +guarantees +infants +gothic +cylinder +witch +buck +indication +eh +congratulations +tba +cohen +sie +usgs +puppy +kathy +acre +graphs +surround +cigarettes +revenge +expires +enemies +lows +controllers +aqua +chen +emma +consultancy +finances +accepts +enjoying +conventions +eva +patrol +smell +pest +hc +italiano +coordinates +rca +fp +carnival +roughly +sticker +promises +responding +reef +physically +divide +stakeholders +hydrocodone +gst +consecutive +cornell +satin +bon +deserve +attempting +mailto +promo +jj +representations +chan +worried +tunes +garbage +competing +combines +mas +beth +bradford +len +phrases +kai +peninsula +chelsea +boring +reynolds +dom +jill +accurately +speeches +reaches +schema +considers +sofa +catalogs +ministries +vacancies +quizzes +parliamentary +obj +prefix +lucia +savannah +barrel +typing +nerve +dans +planets +deficit +boulder +pointing +renew +coupled +viii +myanmar +metadata +harold +circuits +floppy +texture +handbags +jar +ev +somerset +incurred +acknowledge +thoroughly +antigua +nottingham +thunder +tent +caution +identifies +questionnaire +qualification +locks +modelling +namely +miniature +dept +hack +dare +euros +interstate +pirates +aerial +hawk +consequence +rebel +systematic +perceived +origins +hired +makeup +textile +lamb +madagascar +nathan +tobago +presenting +cos +troubleshooting +uzbekistan +indexes +pac +rl +erp +centuries +gl +magnitude +ui +richardson +hindu +dh +fragrances +vocabulary +licking +earthquake +vpn +fundraising +fcc +markers +weights +albania +geological +assessing +lasting +wicked +eds +introduces +kills +roommate +webcams +pushed +webmasters +ro +df +computational +acdbentity +participated +junk +handhelds +wax +lucy +answering +hans +impressed +slope +reggae +failures +poet +conspiracy +surname +theology +nails +evident +whats +rides +rehab +epic +saturn +organizer +nut +allergy +sake +twisted +combinations +preceding +merit +enzyme +cumulative +zshops +planes +edmonton +tackle +disks +condo +pokemon +amplifier +ambien +arbitrary +prominent +retrieve +lexington +vernon +sans +worldcat +titanium +irs +fairy +builds +contacted +shaft +lean +bye +cdt +recorders +occasional +leslie +casio +deutsche +ana +postings +innovations +kitty +postcards +dude +drain +monte +fires +algeria +blessed +luis +reviewing +cardiff +cornwall +favors +potato +panic +explicitly +sticks +leone +transsexual +ez +citizenship +excuse +reforms +basement +onion +strand +pf +sandwich +uw +lawsuit +alto +informative +girlfriend +bloomberg +cheque +hierarchy +influenced +banners +reject +eau +abandoned +bd +circles +italic +beats +merry +mil +scuba +gore +complement +cult +dash +passive +mauritius +valued +cage +checklist +requesting +courage +verde +lauderdale +scenarios +gazette +hitachi +divx +extraction +batman +elevation +hearings +coleman +hugh +lap +utilization +beverages +calibration +jake +eval +efficiently +anaheim +ping +textbook +dried +entertaining +prerequisite +luther +frontier +settle +stopping +refugees +knights +hypothesis +palmer +medicines +flux +derby +sao +peaceful +altered +pontiac +regression +doctrine +scenic +trainers +muze +enhancements +renewable +intersection +passwords +sewing +consistency +collectors +conclude +munich +oman +celebs +gmc +propose +hh +azerbaijan +lighter +rage +adsl +uh +prix +astrology +advisors +pavilion +tactics +trusts +occurring +supplemental +travelling +talented +annie +pillow +induction +derek +precisely +shorter +harley +spreading +provinces +relying +finals +paraguay +steal +parcel +refined +fd +bo +fifteen +widespread +incidence +fears +predict +boutique +acrylic +rolled +tuner +avon +incidents +peterson +rays +asn +shannon +toddler +enhancing +flavor +alike +walt +homeless +horrible +hungry +metallic +acne +blocked +interference +warriors +palestine +listprice +libs +undo +cadillac +atmospheric +malawi +wm +pk +sagem +knowledgestorm +dana +halo +ppm +curtis +parental +referenced +strikes +lesser +publicity +marathon +ant +proposition +gays +pressing +gasoline +apt +dressed +scout +belfast +exec +dealt +niagara +inf +eos +warcraft +charms +catalyst +trader +bucks +allowance +vcr +denial +uri +designation +thrown +prepaid +raises +gem +duplicate +electro +criterion +badge +wrist +civilization +analyzed +vietnamese +heath +tremendous +ballot +lexus +varying +remedies +validity +trustee +maui +weighted +angola +performs +plastics +realm +corrected +jenny +helmet +salaries +postcard +elephant +yemen +encountered +tsunami +scholar +nickel +internationally +surrounded +psi +buses +expedia +geology +pct +wb +creatures +coating +commented +wallet +cleared +smilies +vids +accomplish +boating +drainage +shakira +corners +broader +vegetarian +rouge +yeast +yale +newfoundland +sn +qld +pas +clearing +investigated +dk +ambassador +coated +intend +stephanie +contacting +vegetation +doom +findarticles +louise +kenny +specially +owen +routines +hitting +yukon +beings +bite +issn +aquatic +reliance +habits +striking +myth +infectious +podcasts +singh +gig +gilbert +sas +ferrari +continuity +brook +fu +outputs +phenomenon +ensemble +insulin +assured +biblical +weed +conscious +accent +mysimon +eleven +wives +ambient +utilize +mileage +oecd +prostate +adaptor +auburn +unlock +hyundai +pledge +vampire +angela +relates +nitrogen +xerox +dice +merger +softball +referrals +quad +dock +differently +firewire +mods +nextel +framing +musician +blocking +rwanda +sorts +integrating +vsnet +limiting +dispatch +revisions +papua +restored +hint +armor +riders +chargers +remark +dozens +varies +msie +reasoning +wn +liz +rendered +picking +charitable +guards +annotated +ccd +sv +convinced +openings +buys +burlington +replacing +researcher +watershed +councils +occupations +acknowledged +kruger +pockets +granny +pork +zu +equilibrium +viral +inquire +pipes +characterized +laden +aruba +cottages +realtor +merge +privilege +edgar +develops +qualifying +chassis +dubai +estimation +barn +pushing +llp +fleece +pediatric +boc +fare +dg +asus +pierce +allan +dressing +techrepublic +sperm +vg +bald +filme +craps +fuji +frost +leon +institutes +mold +dame +fo +sally +yacht +tracy +prefers +drilling +brochures +herb +tmp +alot +ate +breach +whale +traveller +appropriations +suspected +tomatoes +benchmark +beginners +instructors +highlighted +bedford +stationery +idle +mustang +unauthorized +clusters +antibody +competent +momentum +fin +wiring +io +pastor +mud +calvin +uni +shark +contributor +demonstrates +phases +grateful +emerald +gradually +laughing +grows +cliff +desirable +tract +ul +ballet +ol +journalist +abraham +js +bumper +afterwards +webpage +religions +garlic +hostels +shine +senegal +explosion +pn +banned +wendy +briefs +signatures +diffs +cove +mumbai +ozone +disciplines +casa +mu +daughters +conversations +radios +tariff +nvidia +opponent +pasta +simplified +muscles +serum +wrapped +swift +motherboard +runtime +inbox +focal +bibliographic +eden +distant +incl +champagne +ala +decimal +hq +deviation +superintendent +propecia +dip +nbc +samba +hostel +housewives +employ +mongolia +penguin +magical +influences +inspections +irrigation +miracle +manually +reprint +reid +wt +hydraulic +centered +robertson +flex +yearly +penetration +wound +belle +rosa +conviction +hash +omissions +writings +hamburg +lazy +mv +mpg +retrieval +qualities +cindy +fathers +carb +charging +cas +marvel +lined +cio +dow +prototype +importantly +rb +petite +apparatus +upc +terrain +dui +pens +explaining +yen +strips +gossip +rangers +nomination +empirical +mh +rotary +worm +dependence +discrete +beginner +boxed +lid +sexuality +polyester +cubic +deaf +commitments +suggesting +sapphire +kinase +skirts +mats +remainder +crawford +labeled +privileges +televisions +specializing +marking +commodities +pvc +serbia +sheriff +griffin +declined +guyana +spies +blah +mime +neighbor +motorcycles +elect +highways +thinkpad +concentrate +intimate +reproductive +preston +deadly +feof +bunny +chevy +molecules +rounds +longest +refrigerator +tions +intervals +sentences +dentists +usda +exclusion +workstation +holocaust +keen +flyer +peas +dosage +receivers +urls +disposition +variance +navigator +investigators +cameroon +baking +marijuana +adaptive +computed +needle +baths +enb +gg +cathedral +brakes +og +nirvana +ko +fairfield +owns +til +invision +sticky +destiny +generous +madness +emacs +climb +blowing +fascinating +landscapes +heated +lafayette +jackie +wto +computation +hay +cardiovascular +ww +sparc +cardiac +salvation +dover +adrian +predictions +accompanying +vatican +brutal +learners +gd +selective +arbitration +configuring +token +editorials +zinc +sacrifice +seekers +guru +isa +removable +convergence +yields +gibraltar +levy +suited +numeric +anthropology +skating +kinda +aberdeen +emperor +grad +malpractice +dylan +bras +belts +blacks +educated +rebates +reporters +burke +proudly +pix +necessity +rendering +mic +inserted +pulling +basename +kyle +obesity +curves +suburban +touring +clara +vertex +bw +hepatitis +nationally +tomato +andorra +waterproof +expired +mj +travels +flush +waiver +pale +specialties +hayes +humanitarian +invitations +functioning +delight +survivor +garcia +cingular +economies +alexandria +bacterial +moses +counted +undertake +declare +continuously +johns +valves +gaps +impaired +achievements +donors +tear +jewel +teddy +lf +convertible +ata +teaches +ventures +nil +bufing +stranger +tragedy +julian +nest +pam +dryer +painful +velvet +tribunal +ruled +nato +pensions +prayers +funky +secretariat +nowhere +cop +paragraphs +gale +joins +adolescent +nominations +wesley +dim +lately +cancelled +scary +mattress +mpegs +brunei +likewise +banana +introductory +slovak +cakes +stan +reservoir +occurrence +idol +mixer +remind +wc +worcester +sbjct +demographic +charming +mai +tooth +disciplinary +annoying +respected +stays +disclose +affair +drove +washer +upset +restrict +springer +beside +mines +portraits +rebound +logan +mentor +interpreted +evaluations +fought +baghdad +elimination +metres +hypothetical +immigrants +complimentary +helicopter +pencil +freeze +hk +performer +abu +titled +commissions +sphere +powerseller +moss +ratios +concord +graduated +endorsed +ty +surprising +walnut +lance +ladder +italia +unnecessary +dramatically +liberia +sherman +cork +maximize +cj +hansen +senators +workout +mali +yugoslavia +bleeding +characterization +colon +likelihood +lanes +purse +fundamentals +contamination +mtv +endangered +compromise +optimize +stating +dome +caroline +leu +expiration +namespace +align +peripheral +bless +engaging +negotiation +crest +opponents +triumph +nominated +confidentiality +electoral +changelog +welding +deferred +alternatively +heel +alloy +condos +plots +polished +yang +gently +greensboro +tulsa +locking +casey +controversial +draws +fridge +blanket +bloom +qc +simpsons +lou +elliott +recovered +fraser +justify +upgrading +blades +pgp +loops +surge +frontpage +trauma +aw +tahoe +advert +possess +demanding +defensive +sip +flashers +subaru +forbidden +tf +vanilla +programmers +pj +monitored +installations +deutschland +picnic +souls +arrivals +spank +cw +practitioner +motivated +wr +dumb +smithsonian +hollow +vault +securely +examining +fioricet +groove +revelation +rg +pursuit +delegation +wires +bl +dictionaries +mails +backing +greenhouse +sleeps +vc +blake +transparency +dee +travis +wx +endless +figured +orbit +currencies +niger +bacon +survivors +positioning +heater +colony +cannon +circus +promoted +forbes +mae +moldova +mel +descending +paxil +spine +trout +enclosed +feat +temporarily +ntsc +cooked +thriller +transmit +apnic +fatty +gerald +pressed +frequencies +scanned +reflections +hunger +mariah +sic +municipality +usps +joyce +detective +surgeon +cement +experiencing +fireplace +endorsement +bg +planners +disputes +textiles +missile +intranet +closes +seq +psychiatry +persistent +deborah +conf +marco +assists +summaries +glow +gabriel +auditor +wma +aquarium +violin +prophet +cir +bracket +looksmart +isaac +oxide +oaks +magnificent +erik +colleague +naples +promptly +modems +adaptation +hu +harmful +paintball +prozac +sexually +enclosure +acm +dividend +newark +kw +paso +glucose +phantom +norm +playback +supervisors +westminster +turtle +ips +distances +absorption +treasures +dsc +warned +neural +ware +fossil +mia +hometown +badly +transcripts +apollo +wan +disappointed +persian +continually +communist +collectible +handmade +greene +entrepreneurs +robots +grenada +creations +jade +scoop +acquisitions +foul +keno +gtk +earning +mailman +sanyo +nested +biodiversity +excitement +somalia +movers +verbal +blink +presently +seas +carlo +workflow +mysterious +novelty +bryant +tiles +voyuer +librarian +subsidiaries +switched +stockholm +tamil +garmin +ru +pose +fuzzy +indonesian +grams +therapist +richards +mrna +budgets +toolkit +promising +relaxation +goat +render +carmen +ira +sen +thereafter +hardwood +erotica +temporal +sail +forge +commissioners +dense +dts +brave +forwarding +qt +awful +nightmare +airplane +reductions +southampton +istanbul +impose +organisms +sega +telescope +viewers +asbestos +portsmouth +cdna +meyer +enters +pod +savage +advancement +wu +harassment +willow +resumes +bolt +gage +throwing +existed +generators +lu +wagon +barbie +dat +soa +knock +urge +smtp +generates +potatoes +thorough +replication +inexpensive +kurt +receptors +peers +roland +optimum +neon +interventions +quilt +huntington +creature +ours +mounts +syracuse +internship +lone +refresh +aluminium +snowboard +beastality +webcast +michel +evanescence +subtle +coordinated +notre +shipments +maldives +stripes +firmware +antarctica +cope +shepherd +lm +canberra +cradle +chancellor +mambo +lime +kirk +flour +controversy +legendary +bool +sympathy +choir +avoiding +beautifully +blond +expects +cho +jumping +fabrics +antibodies +polymer +hygiene +wit +poultry +virtue +burst +examinations +surgeons +bouquet +immunology +promotes +mandate +wiley +departmental +bbs +spas +ind +corpus +johnston +terminology +gentleman +fibre +reproduce +convicted +shades +jets +indices +roommates +adware +qui +intl +threatening +spokesman +zoloft +activists +frankfurt +prisoner +daisy +halifax +encourages +ultram +cursor +assembled +earliest +donated +stuffed +restructuring +insects +terminals +crude +morrison +maiden +simulations +cz +sufficiently +examines +viking +myrtle +bored +cleanup +yarn +knit +conditional +mug +crossword +bother +budapest +conceptual +knitting +attacked +hl +bhutan +liechtenstein +mating +compute +redhead +arrives +translator +automobiles +tractor +allah +continent +ob +unwrap +fares +longitude +resist +challenged +telecharger +hoped +pike +safer +insertion +instrumentation +ids +hugo +wagner +constraint +groundwater +touched +strengthening +cologne +gzip +wishing +ranger +smallest +insulation +newman +marsh +ricky +ctrl +scared +theta +infringement +bent +laos +subjective +monsters +asylum +lightbox +robbie +stake +cocktail +outlets +swaziland +varieties +arbor +mediawiki +configurations +poison diff --git a/public/vocab_en_es.txt b/public/vocab_en_es.txt new file mode 100644 index 0000000000000000000000000000000000000000..058be9106969b5dd8ebdcfe26978868981c3f444 --- /dev/null +++ b/public/vocab_en_es.txt @@ -0,0 +1,10338 @@ +the +of +and +to +in +for +is +on +that +by +this +with +you +it +not +or +be +are +from +at +as +your +all +have +new +more +an +was +we +will +home +can +us +about +if +page +my +has +search +free +but +our +one +other +do +no +information +time +they +site +he +up +may +what +which +their +news +out +use +any +there +see +only +so +his +when +contact +here +business +who +web +also +now +help +get +pm +view +online +first +am +been +would +how +were +me +services +some +these +click +its +like +service +than +find +price +date +back +top +people +had +list +name +just +over +state +year +day +into +email +two +health +world +re +next +used +go +work +last +most +products +music +buy +data +make +them +should +product +system +post +her +city +add +policy +number +such +please +available +copyright +support +message +after +best +software +then +jan +good +video +well +where +info +rights +public +books +high +school +through +each +links +she +review +years +order +very +privacy +book +items +company +read +group +need +many +user +said +de +does +set +under +general +research +university +january +mail +full +map +reviews +program +life +know +games +way +days +management +part +could +great +united +hotel +real +item +international +center +ebay +must +store +travel +comments +made +development +report +off +member +details +line +terms +before +hotels +did +send +right +type +because +local +those +using +results +office +education +national +car +design +take +posted +internet +address +community +within +states +area +want +phone +dvd +shipping +reserved +subject +between +forum +family +long +based +code +show +even +black +check +special +prices +website +index +being +women +much +sign +file +link +open +today +technology +south +case +project +same +pages +uk +version +section +own +found +sports +house +related +security +both +county +american +photo +game +members +power +while +care +network +down +computer +systems +three +total +place +end +following +download +him +without +per +access +think +north +resources +current +posts +big +media +law +control +water +history +pictures +size +art +personal +since +including +guide +shop +directory +board +location +change +white +text +small +rating +rate +government +children +during +usa +return +students +shopping +account +times +sites +level +digital +profile +previous +form +events +love +old +john +main +call +hours +image +department +title +description +non +insurance +another +why +shall +property +class +cd +still +money +quality +every +listing +content +country +private +little +visit +save +tools +low +reply +customer +december +compare +movies +include +college +value +article +york +man +card +jobs +provide +food +source +author +different +press +learn +sale +around +print +course +job +canada +process +teen +room +stock +training +too +credit +point +join +science +men +categories +advanced +west +sales +look +english +left +team +estate +box +conditions +select +windows +photos +gay +thread +week +category +note +live +large +gallery +table +register +however +june +october +november +market +library +really +action +start +series +model +features +air +industry +plan +human +provided +tv +yes +required +second +hot +accessories +cost +movie +forums +march +la +september +better +say +questions +july +yahoo +going +medical +test +friend +come +dec +server +pc +study +application +cart +staff +articles +san +feedback +again +play +looking +issues +april +never +users +complete +street +topic +comment +financial +things +working +against +standard +tax +person +below +mobile +less +got +blog +party +payment +equipment +login +student +let +programs +offers +legal +above +recent +park +stores +side +act +problem +red +give +memory +performance +social +august +quote +language +story +sell +options +experience +rates +create +key +body +young +america +important +field +few +east +paper +single +ii +age +activities +club +example +girls +additional +password +latest +something +road +gift +question +changes +night +ca +hard +texas +oct +pay +four +poker +status +browse +issue +range +building +seller +court +february +always +result +audio +light +write +war +nov +offer +blue +groups +al +easy +given +files +event +release +analysis +request +fax +china +making +picture +needs +possible +might +professional +yet +month +major +star +areas +future +space +committee +hand +sun +cards +problems +london +washington +meeting +rss +become +interest +id +child +keep +enter +california +share +similar +garden +schools +million +added +reference +companies +listed +baby +learning +energy +run +delivery +net +popular +term +film +stories +put +computers +journal +reports +co +try +welcome +central +images +president +notice +original +head +radio +until +cell +color +self +council +away +includes +track +australia +discussion +archive +once +others +entertainment +agreement +format +least +society +months +log +safety +friends +sure +faq +trade +edition +cars +messages +marketing +tell +further +updated +association +able +having +provides +david +fun +already +green +studies +close +common +drive +specific +several +gold +feb +living +sep +collection +called +short +arts +lot +ask +display +limited +powered +solutions +means +director +daily +beach +past +natural +whether +due +et +electronics +five +upon +period +planning +database +says +official +weather +mar +land +average +done +technical +window +france +pro +region +island +record +direct +microsoft +conference +environment +records +st +district +calendar +costs +style +url +front +statement +update +parts +aug +ever +downloads +early +miles +sound +resource +present +applications +either +ago +document +word +works +material +bill +apr +written +talk +federal +hosting +rules +final +adult +tickets +thing +centre +requirements +via +cheap +kids +finance +true +minutes +else +mark +third +rock +gifts +europe +reading +topics +bad +individual +tips +plus +auto +cover +usually +edit +together +videos +percent +fast +function +fact +unit +getting +global +tech +meet +far +economic +en +player +projects +lyrics +often +subscribe +submit +germany +amount +watch +included +feel +though +bank +risk +thanks +everything +deals +various +words +linux +jul +production +commercial +james +weight +town +heart +advertising +received +choose +treatment +newsletter +archives +points +knowledge +magazine +error +camera +jun +girl +currently +construction +toys +registered +clear +golf +receive +domain +methods +chapter +makes +protection +policies +loan +wide +beauty +manager +india +position +taken +sort +listings +models +michael +known +half +cases +step +engineering +florida +simple +quick +none +wireless +license +paul +friday +lake +whole +annual +published +later +basic +sony +shows +corporate +google +church +method +purchase +customers +active +response +practice +hardware +figure +materials +fire +holiday +chat +enough +designed +along +among +death +writing +speed +html +countries +loss +face +brand +discount +higher +effects +created +remember +standards +oil +bit +yellow +political +increase +advertise +kingdom +base +near +environmental +thought +stuff +french +storage +oh +japan +doing +loans +shoes +entry +stay +nature +orders +availability +africa +summary +turn +mean +growth +notes +agency +king +monday +european +activity +copy +although +drug +pics +western +income +force +cash +employment +overall +bay +river +commission +ad +package +contents +seen +players +engine +port +album +regional +stop +supplies +started +administration +bar +institute +views +plans +double +dog +build +screen +exchange +types +soon +sponsored +lines +electronic +continue +across +benefits +needed +season +apply +someone +held +ny +anything +printer +condition +effective +believe +organization +effect +asked +eur +mind +sunday +selection +casino +pdf +lost +tour +menu +volume +cross +anyone +mortgage +hope +silver +corporation +wish +inside +solution +mature +role +rather +weeks +addition +came +supply +nothing +certain +usr +executive +running +lower +necessary +union +jewelry +according +dc +clothing +mon +com +particular +fine +names +robert +homepage +hour +gas +skills +six +bush +islands +advice +career +military +rental +decision +leave +british +teens +pre +huge +sat +woman +facilities +zip +bid +kind +sellers +middle +move +cable +opportunities +taking +values +division +coming +tuesday +object +lesbian +appropriate +machine +logo +length +actually +nice +score +statistics +client +ok +returns +capital +follow +sample +investment +sent +shown +saturday +christmas +england +culture +band +flash +ms +lead +george +choice +went +starting +registration +fri +thursday +courses +consumer +hi +airport +foreign +artist +outside +furniture +levels +channel +letter +mode +phones +ideas +wednesday +structure +fund +summer +allow +degree +contract +button +releases +wed +homes +super +male +matter +custom +virginia +almost +took +located +multiple +asian +distribution +editor +inn +industrial +cause +potential +song +cnet +ltd +los +hp +focus +late +fall +featured +idea +rooms +female +responsible +inc +communications +win +associated +thomas +primary +cancer +numbers +reason +tool +browser +spring +foundation +answer +voice +eg +friendly +schedule +documents +communication +purpose +feature +bed +comes +police +everyone +independent +ip +approach +cameras +brown +physical +operating +hill +maps +medicine +deal +hold +ratings +chicago +forms +glass +happy +tue +smith +wanted +developed +thank +safe +unique +survey +prior +telephone +sport +ready +feed +animal +sources +mexico +population +pa +regular +secure +navigation +operations +therefore +simply +evidence +station +christian +round +paypal +favorite +understand +option +master +valley +recently +probably +thu +rentals +sea +built +publications +blood +cut +worldwide +improve +connection +publisher +hall +larger +anti +networks +earth +parents +nokia +impact +transfer +introduction +kitchen +strong +tel +carolina +wedding +properties +hospital +ground +overview +ship +accommodation +owners +disease +tx +excellent +paid +italy +perfect +hair +opportunity +kit +classic +basis +command +cities +william +express +award +distance +tree +peter +assessment +ensure +thus +wall +ie +involved +el +extra +especially +interface +partners +budget +rated +guides +success +maximum +ma +operation +existing +quite +selected +boy +amazon +patients +restaurants +beautiful +warning +wine +locations +horse +vote +forward +flowers +stars +significant +lists +technologies +owner +retail +animals +useful +directly +manufacturer +ways +est +son +providing +rule +mac +housing +takes +iii +gmt +bring +catalog +searches +max +trying +mother +authority +considered +told +xml +traffic +programme +joined +input +strategy +feet +agent +valid +bin +modern +senior +ireland +teaching +door +grand +testing +trial +charge +units +instead +canadian +cool +normal +wrote +enterprise +ships +entire +educational +md +leading +metal +positive +fl +fitness +chinese +opinion +mb +asia +football +abstract +uses +output +funds +mr +greater +likely +develop +employees +artists +alternative +processing +responsibility +resolution +java +guest +seems +publication +pass +relations +trust +van +contains +session +multi +photography +republic +fees +components +vacation +century +academic +assistance +completed +skin +graphics +indian +prev +ads +mary +il +expected +ring +grade +dating +pacific +mountain +organizations +pop +filter +mailing +vehicle +longer +consider +int +northern +behind +panel +floor +german +buying +match +proposed +default +require +iraq +boys +outdoor +deep +morning +otherwise +allows +rest +protein +plant +reported +hit +transportation +mm +pool +mini +politics +partner +disclaimer +authors +boards +faculty +parties +fish +membership +mission +eye +string +sense +modified +pack +released +stage +internal +goods +recommended +born +unless +richard +detailed +japanese +race +approved +background +target +except +character +usb +maintenance +ability +maybe +functions +ed +moving +brands +places +php +pretty +trademarks +phentermine +spain +southern +yourself +etc +winter +battery +youth +pressure +submitted +boston +debt +keywords +medium +television +interested +core +break +purposes +throughout +sets +dance +wood +msn +itself +defined +papers +playing +awards +fee +studio +reader +virtual +device +established +answers +rent +las +remote +dark +programming +external +apple +le +regarding +instructions +min +offered +theory +enjoy +remove +aid +surface +minimum +visual +host +variety +teachers +isbn +martin +manual +block +subjects +agents +increased +repair +fair +civil +steel +understanding +songs +fixed +wrong +beginning +hands +associates +finally +az +updates +desktop +classes +paris +ohio +gets +sector +capacity +requires +jersey +un +fat +fully +father +electric +saw +instruments +quotes +officer +driver +businesses +dead +respect +unknown +specified +restaurant +mike +trip +pst +worth +mi +procedures +poor +teacher +eyes +relationship +workers +farm +georgia +peace +traditional +campus +tom +showing +creative +coast +benefit +progress +funding +devices +lord +grant +sub +agree +fiction +hear +sometimes +watches +careers +beyond +goes +families +led +museum +themselves +fan +transport +interesting +blogs +wife +evaluation +accepted +former +implementation +ten +hits +zone +complex +th +cat +galleries +references +die +presented +jack +flat +flow +agencies +literature +respective +parent +spanish +michigan +columbia +setting +dr +scale +stand +economy +highest +helpful +monthly +critical +frame +musical +definition +secretary +angeles +networking +path +australian +employee +chief +gives +kb +bottom +magazines +packages +detail +francisco +laws +changed +pet +heard +begin +individuals +colorado +royal +clean +switch +russian +largest +african +guy +titles +relevant +guidelines +justice +connect +bible +dev +cup +basket +applied +weekly +vol +installation +described +demand +pp +suite +vegas +na +square +chris +attention +advance +skip +diet +army +auction +gear +lee +os +difference +allowed +correct +charles +nation +selling +lots +piece +sheet +firm +seven +older +illinois +regulations +elements +species +jump +cells +module +resort +facility +random +pricing +dvds +certificate +minister +motion +looks +fashion +directions +visitors +documentation +monitor +trading +forest +calls +whose +coverage +couple +giving +chance +vision +ball +ending +clients +actions +listen +discuss +accept +automotive +naked +goal +successful +sold +wind +communities +clinical +situation +sciences +markets +lowest +highly +publishing +appear +emergency +developing +lives +currency +leather +determine +temperature +palm +announcements +patient +actual +historical +stone +bob +commerce +ringtones +perhaps +persons +difficult +scientific +satellite +fit +tests +village +accounts +amateur +ex +met +pain +xbox +particularly +factors +coffee +www +settings +buyer +cultural +steve +easily +oral +ford +poster +edge +functional +root +au +fi +closed +holidays +ice +pink +zealand +balance +monitoring +graduate +replies +shot +nc +architecture +initial +label +thinking +scott +llc +sec +recommend +canon +league +waste +minute +bus +provider +optional +dictionary +cold +accounting +manufacturing +sections +chair +fishing +effort +phase +fields +bag +fantasy +po +letters +motor +va +professor +context +install +shirt +apparel +generally +continued +foot +mass +crime +count +breast +techniques +ibm +rd +johnson +sc +quickly +dollars +websites +religion +claim +driving +permission +surgery +patch +heat +wild +measures +generation +kansas +miss +chemical +doctor +task +reduce +brought +himself +nor +component +enable +exercise +bug +santa +mid +guarantee +leader +diamond +israel +se +processes +soft +servers +alone +meetings +seconds +jones +arizona +keyword +interests +flight +congress +fuel +username +walk +produced +italian +paperback +classifieds +wait +supported +pocket +saint +rose +freedom +argument +competition +creating +jim +drugs +joint +premium +providers +fresh +characters +attorney +upgrade +di +factor +growing +thousands +km +stream +apartments +pick +hearing +eastern +auctions +therapy +entries +dates +generated +signed +upper +administrative +serious +prime +samsung +limit +began +louis +steps +errors +shops +del +efforts +informed +ga +ac +thoughts +creek +ft +worked +quantity +urban +practices +sorted +reporting +essential +myself +tours +platform +load +affiliate +labor +immediately +admin +nursing +defense +machines +designated +tags +heavy +covered +recovery +joe +guys +integrated +configuration +merchant +comprehensive +expert +universal +protect +drop +solid +cds +presentation +languages +became +orange +compliance +vehicles +prevent +theme +rich +im +campaign +marine +improvement +vs +guitar +finding +pennsylvania +examples +ipod +saying +spirit +ar +claims +challenge +motorola +acceptance +strategies +mo +seem +affairs +touch +intended +towards +sa +goals +hire +election +suggest +branch +charges +serve +affiliates +reasons +magic +mount +smart +talking +gave +ones +latin +multimedia +xp +avoid +certified +manage +corner +rank +computing +oregon +element +birth +virus +abuse +interactive +requests +separate +quarter +procedure +leadership +tables +define +racing +religious +facts +breakfast +kong +column +plants +faith +chain +developer +identify +avenue +missing +died +approximately +domestic +sitemap +recommendations +moved +houston +reach +comparison +mental +viewed +moment +extended +sequence +inch +attack +sorry +centers +opening +damage +lab +reserve +recipes +cvs +gamma +plastic +produce +snow +placed +truth +counter +failure +follows +eu +weekend +dollar +camp +ontario +automatically +des +minnesota +films +bridge +native +fill +williams +movement +printing +baseball +owned +approval +draft +chart +played +contacts +cc +jesus +readers +clubs +lcd +wa +jackson +equal +adventure +matching +offering +shirts +profit +leaders +posters +institutions +assistant +variable +ave +dj +advertisement +expect +parking +headlines +yesterday +compared +determined +wholesale +workshop +russia +gone +codes +kinds +extension +seattle +statements +golden +completely +teams +fort +cm +wi +lighting +senate +forces +funny +brother +gene +turned +portable +tried +electrical +applicable +disc +returned +pattern +ct +boat +named +theatre +laser +earlier +manufacturers +sponsor +classical +icon +warranty +dedicated +indiana +direction +harry +basketball +objects +ends +delete +evening +assembly +nuclear +taxes +mouse +signal +criminal +issued +brain +sexual +wisconsin +powerful +dream +obtained +false +da +cast +flower +felt +personnel +passed +supplied +identified +falls +pic +soul +aids +opinions +promote +stated +stats +hawaii +professionals +appears +carry +flag +decided +nj +covers +hr +em +advantage +hello +designs +maintain +tourism +priority +newsletters +adults +clips +savings +iv +graphic +atom +payments +rw +estimated +binding +brief +ended +winning +eight +anonymous +iron +straight +script +served +wants +miscellaneous +prepared +void +dining +alert +integration +atlanta +dakota +tag +interview +mix +framework +disk +installed +queen +vhs +credits +clearly +fix +handle +sweet +desk +criteria +pubmed +dave +massachusetts +diego +hong +vice +associate +ne +truck +behavior +enlarge +ray +frequently +revenue +measure +changing +votes +du +duty +looked +discussions +bear +gain +festival +laboratory +ocean +flights +experts +signs +lack +depth +iowa +whatever +logged +laptop +vintage +train +exactly +dry +explore +maryland +spa +concept +nearly +eligible +checkout +reality +forgot +handling +origin +knew +gaming +feeds +billion +destination +scotland +faster +intelligence +dallas +bought +con +ups +nations +route +followed +specifications +broken +tripadvisor +frank +alaska +zoom +blow +battle +residential +anime +speak +decisions +industries +protocol +query +clip +partnership +editorial +nt +expression +es +equity +provisions +speech +wire +principles +suggestions +rural +shared +sounds +replacement +tape +strategic +judge +spam +economics +acid +bytes +cent +forced +compatible +fight +apartment +height +null +zero +speaker +filed +gb +netherlands +obtain +bc +consulting +recreation +offices +designer +remain +managed +pr +failed +marriage +roll +korea +banks +fr +participants +secret +bath +aa +kelly +leads +negative +austin +favorites +toronto +theater +springs +missouri +andrew +var +perform +healthy +translation +estimates +font +assets +injury +mt +joseph +ministry +drivers +lawyer +figures +married +protected +proposal +sharing +philadelphia +portal +waiting +birthday +beta +fail +gratis +banking +officials +brian +toward +won +slightly +assist +conduct +contained +lingerie +legislation +calling +parameters +jazz +serving +bags +profiles +miami +comics +matters +houses +doc +postal +relationships +tennessee +wear +controls +breaking +combined +ultimate +wales +representative +frequency +introduced +minor +finish +departments +residents +noted +displayed +mom +reduced +physics +rare +spent +performed +extreme +samples +davis +daniel +bars +reviewed +row +oz +forecast +removed +helps +singles +administrator +cycle +amounts +contain +accuracy +dual +rise +usd +sleep +mg +bird +pharmacy +brazil +creation +static +scene +hunter +addresses +lady +crystal +famous +writer +chairman +violence +fans +oklahoma +speakers +drink +academy +dynamic +gender +eat +permanent +agriculture +dell +cleaning +constitutes +portfolio +practical +delivered +collectibles +infrastructure +exclusive +seat +concerns +vendor +originally +intel +utilities +philosophy +regulation +officers +reduction +aim +bids +referred +supports +nutrition +recording +regions +junior +toll +les +cape +ann +rings +meaning +tip +secondary +wonderful +mine +ladies +henry +ticket +announced +guess +agreed +prevention +whom +ski +soccer +math +import +posting +presence +instant +mentioned +automatic +healthcare +viewing +maintained +ch +increasing +majority +connected +christ +dan +dogs +sd +directors +aspects +austria +ahead +moon +participation +scheme +utility +preview +fly +manner +matrix +containing +combination +devel +amendment +despite +strength +guaranteed +turkey +libraries +proper +distributed +degrees +singapore +enterprises +delta +fear +seeking +inches +phoenix +rs +convention +shares +principal +daughter +standing +comfort +colors +wars +cisco +ordering +kept +alpha +appeal +cruise +bonus +certification +previously +hey +bookmark +buildings +specials +beat +disney +household +batteries +adobe +smoking +bbc +becomes +drives +arms +alabama +tea +improved +trees +avg +achieve +positions +dress +subscription +dealer +contemporary +sky +utah +nearby +rom +carried +happen +exposure +panasonic +hide +permalink +signature +gambling +refer +miller +provision +outdoors +clothes +caused +luxury +babes +frames +certainly +indeed +newspaper +toy +circuit +layer +printed +slow +removal +easier +src +liability +trademark +hip +printers +faqs +nine +adding +kentucky +mostly +eric +spot +taylor +trackback +prints +spend +factory +interior +revised +grow +americans +optical +promotion +relative +amazing +clock +dot +hiv +identity +suites +conversion +feeling +hidden +reasonable +victoria +serial +relief +revision +broadband +influence +ratio +pda +importance +rain +onto +dsl +planet +webmaster +copies +recipe +zum +permit +seeing +proof +dna +diff +tennis +bass +prescription +bedroom +empty +instance +hole +pets +ride +licensed +orlando +specifically +tim +bureau +maine +sql +represent +conservation +pair +ideal +specs +recorded +don +pieces +finished +parks +dinner +lawyers +sydney +stress +cream +ss +runs +trends +yeah +discover +ap +patterns +boxes +louisiana +hills +javascript +fourth +nm +advisor +mn +marketplace +nd +evil +aware +wilson +shape +evolution +irish +certificates +objectives +stations +suggested +gps +op +remains +acc +greatest +firms +concerned +euro +operator +structures +generic +encyclopedia +usage +cap +ink +charts +continuing +mixed +census +interracial +peak +tn +competitive +exist +wheel +transit +suppliers +salt +compact +poetry +lights +tracking +angel +bell +keeping +preparation +attempt +receiving +matches +accordance +width +noise +engines +forget +array +discussed +accurate +stephen +elizabeth +climate +reservations +pin +playstation +alcohol +greek +instruction +managing +annotation +sister +raw +differences +walking +explain +smaller +newest +establish +gnu +happened +expressed +jeff +extent +sharp +lesbians +ben +lane +paragraph +kill +mathematics +aol +compensation +ce +export +managers +aircraft +modules +sweden +conflict +conducted +versions +employer +occur +percentage +knows +mississippi +describe +concern +backup +requested +citizens +connecticut +heritage +personals +immediate +holding +trouble +spread +coach +kevin +agricultural +expand +supporting +audience +assigned +jordan +collections +ages +participate +plug +specialist +cook +affect +virgin +experienced +investigation +raised +hat +institution +directed +dealers +searching +sporting +helping +perl +affected +lib +bike +totally +plate +expenses +indicate +blonde +ab +proceedings +transmission +anderson +utc +characteristics +der +lose +organic +seek +experiences +albums +cheats +extremely +verzeichnis +contracts +guests +hosted +diseases +concerning +developers +equivalent +chemistry +tony +neighborhood +nevada +kits +thailand +variables +agenda +anyway +continues +tracks +advisory +cam +curriculum +logic +template +prince +circle +soil +grants +anywhere +psychology +responses +atlantic +wet +circumstances +edward +investor +identification +ram +leaving +wildlife +appliances +matt +elementary +cooking +speaking +sponsors +fox +unlimited +respond +sizes +plain +exit +entered +iran +arm +keys +launch +wave +checking +costa +belgium +printable +holy +acts +guidance +mesh +trail +enforcement +symbol +crafts +highway +buddy +hardcover +observed +dean +setup +poll +booking +glossary +fiscal +celebrity +styles +denver +unix +filled +bond +channels +ericsson +appendix +notify +blues +chocolate +pub +portion +scope +hampshire +supplier +cables +cotton +bluetooth +controlled +requirement +authorities +biology +dental +killed +border +ancient +debate +representatives +starts +pregnancy +causes +arkansas +biography +leisure +attractions +learned +transactions +notebook +explorer +historic +attached +opened +tm +husband +disabled +authorized +crazy +upcoming +britain +concert +retirement +scores +financing +efficiency +sp +comedy +adopted +efficient +weblog +linear +commitment +specialty +bears +jean +hop +carrier +edited +constant +visa +mouth +jewish +meter +linked +portland +interviews +concepts +nh +gun +reflect +pure +deliver +wonder +lessons +fruit +begins +qualified +reform +lens +alerts +treated +discovery +draw +mysql +classified +relating +assume +confidence +alliance +fm +confirm +warm +neither +lewis +howard +offline +leaves +engineer +lifestyle +consistent +replace +clearance +connections +inventory +converter +organisation +babe +checks +reached +becoming +safari +objective +indicated +sugar +crew +legs +sam +stick +securities +allen +pdt +relation +enabled +genre +slide +montana +volunteer +tested +rear +democratic +enhance +switzerland +exact +bound +parameter +adapter +processor +node +formal +dimensions +contribute +lock +hockey +storm +micro +colleges +laptops +mile +showed +challenges +editors +mens +threads +bowl +supreme +brothers +recognition +presents +ref +tank +submission +dolls +estimate +encourage +navy +kid +regulatory +inspection +consumers +cancel +limits +territory +transaction +manchester +weapons +paint +delay +pilot +outlet +contributions +continuous +db +czech +resulting +cambridge +initiative +novel +pan +execution +disability +increases +ultra +winner +idaho +contractor +ph +episode +examination +potter +dish +plays +bulletin +ia +pt +indicates +modify +oxford +adam +truly +epinions +painting +committed +extensive +affordable +universe +candidate +databases +patent +slot +psp +outstanding +ha +eating +perspective +planned +watching +lodge +messenger +mirror +tournament +consideration +ds +discounts +sterling +sessions +kernel +stocks +buyers +journals +gray +catalogue +ea +jennifer +antonio +charged +broad +taiwan +und +chosen +demo +greece +lg +swiss +sarah +clark +hate +terminal +publishers +nights +behalf +caribbean +liquid +rice +nebraska +loop +salary +reservation +foods +gourmet +guard +properly +orleans +saving +nfl +remaining +empire +resume +twenty +newly +raise +prepare +avatar +gary +depending +illegal +expansion +vary +hundreds +rome +arab +lincoln +helped +premier +tomorrow +purchased +milk +decide +consent +drama +visiting +performing +downtown +keyboard +contest +collected +nw +bands +boot +suitable +ff +absolutely +millions +lunch +audit +push +chamber +guinea +findings +muscle +featuring +iso +implement +clicking +scheduled +polls +typical +tower +yours +sum +misc +calculator +significantly +chicken +temporary +attend +shower +alan +sending +jason +tonight +dear +sufficient +holdem +shell +province +catholic +oak +vat +awareness +vancouver +governor +beer +seemed +contribution +measurement +swimming +spyware +formula +constitution +packaging +solar +jose +catch +jane +pakistan +ps +reliable +consultation +northwest +sir +doubt +earn +finder +unable +periods +classroom +tasks +democracy +attacks +kim +wallpaper +merchandise +const +resistance +doors +symptoms +resorts +biggest +memorial +visitor +twin +forth +insert +baltimore +gateway +ky +dont +alumni +drawing +candidates +charlotte +ordered +biological +fighting +transition +happens +preferences +spy +romance +instrument +bruce +split +themes +powers +heaven +br +bits +pregnant +twice +classification +focused +egypt +physician +hollywood +bargain +wikipedia +cellular +norway +vermont +asking +blocks +normally +lo +spiritual +hunting +diabetes +suit +ml +shift +chip +res +sit +bodies +photographs +cutting +wow +simon +writers +marks +flexible +loved +mapping +numerous +relatively +birds +satisfaction +represents +char +indexed +pittsburgh +superior +preferred +saved +paying +cartoon +shots +intellectual +moore +granted +choices +carbon +spending +comfortable +magnetic +interaction +listening +effectively +registry +crisis +outlook +massive +denmark +employed +bright +treat +header +cs +poverty +formed +piano +echo +que +grid +sheets +patrick +experimental +puerto +revolution +consolidation +displays +plasma +allowing +earnings +voip +mystery +landscape +dependent +mechanical +journey +delaware +bidding +consultants +risks +banner +applicant +charter +fig +barbara +cooperation +counties +acquisition +ports +implemented +sf +directories +recognized +dreams +blogger +notification +kg +licensing +stands +teach +occurred +textbooks +rapid +pull +hairy +diversity +cleveland +ut +reverse +deposit +seminar +investments +latina +nasa +wheels +specify +accessibility +dutch +sensitive +templates +formats +tab +depends +boots +holds +router +concrete +si +editing +poland +folder +womens +css +completion +upload +pulse +universities +technique +contractors +voting +courts +notices +subscriptions +calculate +mc +detroit +alexander +broadcast +converted +metro +toshiba +anniversary +improvements +strip +specification +pearl +accident +nick +accessible +accessory +resident +plot +qty +possibly +airline +typically +representation +regard +pump +exists +arrangements +smooth +conferences +uniprotkb +strike +consumption +birmingham +flashing +lp +narrow +afternoon +threat +surveys +sitting +putting +consultant +controller +ownership +committees +legislative +researchers +vietnam +trailer +anne +castle +gardens +missed +malaysia +unsubscribe +antique +labels +willing +bio +molecular +acting +heads +stored +exam +logos +residence +attorneys +antiques +density +hundred +ryan +operators +strange +sustainable +philippines +statistical +beds +mention +innovation +pcs +employers +grey +parallel +honda +amended +operate +bills +bold +bathroom +stable +opera +definitions +von +doctors +lesson +cinema +asset +ag +scan +elections +drinking +reaction +blank +enhanced +entitled +severe +generate +stainless +newspapers +hospitals +vi +deluxe +humor +aged +monitors +exception +lived +duration +bulk +successfully +indonesia +pursuant +sci +fabric +edt +visits +primarily +tight +domains +capabilities +pmid +contrast +recommendation +flying +recruitment +sin +berlin +cute +organized +ba +para +siemens +adoption +improving +cr +expensive +meant +capture +pounds +buffalo +organisations +plane +pg +explained +seed +programmes +desire +expertise +mechanism +camping +ee +jewellery +meets +welfare +peer +caught +eventually +marked +driven +measured +medline +bottle +agreements +considering +innovative +marshall +massage +rubber +conclusion +closing +tampa +thousand +meat +legend +grace +susan +ing +ks +adams +python +monster +alex +bang +villa +bone +columns +disorders +bugs +collaboration +hamilton +detection +ftp +cookies +inner +formation +tutorial +med +engineers +entity +cruises +gate +holder +proposals +moderator +sw +tutorials +settlement +portugal +lawrence +roman +duties +valuable +tone +collectables +ethics +forever +dragon +busy +captain +fantastic +imagine +brings +heating +leg +neck +hd +wing +governments +purchasing +scripts +abc +stereo +appointed +taste +dealing +commit +tiny +operational +rail +airlines +liberal +livecam +jay +trips +gap +sides +tube +turns +corresponding +descriptions +cache +belt +jacket +determination +animation +oracle +er +matthew +lease +productions +aviation +hobbies +proud +excess +disaster +console +commands +jr +telecommunications +instructor +giant +achieved +injuries +shipped +seats +approaches +biz +alarm +voltage +anthony +nintendo +usual +loading +stamps +appeared +franklin +angle +rob +vinyl +highlights +mining +designers +melbourne +ongoing +worst +imaging +betting +scientists +liberty +wyoming +blackjack +argentina +era +convert +possibility +analyst +commissioner +dangerous +garage +exciting +reliability +thongs +gcc +unfortunately +respectively +volunteers +attachment +ringtone +finland +morgan +derived +pleasure +honor +asp +oriented +eagle +desktops +pants +columbus +nurse +prayer +appointment +workshops +hurricane +quiet +luck +postage +producer +represented +mortgages +dial +responsibilities +cheese +comic +carefully +jet +productivity +investors +crown +par +underground +diagnosis +maker +crack +principle +picks +vacations +gang +semester +calculated +fetish +applies +casinos +appearance +smoke +apache +filters +incorporated +nv +craft +cake +notebooks +apart +fellow +blind +lounge +mad +algorithm +semi +coins +andy +gross +strongly +cafe +valentine +hilton +ken +proteins +horror +su +exp +familiar +capable +douglas +debian +till +involving +pen +investing +christopher +admission +epson +shoe +elected +carrying +victory +sand +madison +terrorism +joy +editions +cpu +mainly +ethnic +ran +parliament +actor +finds +seal +situations +fifth +allocated +citizen +vertical +corrections +structural +municipal +describes +prize +sr +occurs +jon +absolute +disabilities +consists +anytime +substance +prohibited +addressed +lies +pipe +soldiers +nr +guardian +lecture +simulation +layout +initiatives +ill +concentration +classics +lbs +lay +interpretation +horses +lol +dirty +deck +wayne +donate +taught +bankruptcy +mp +worker +optimization +alive +temple +substances +prove +discovered +wings +breaks +genetic +restrictions +participating +waters +promise +thin +exhibition +prefer +ridge +cabinet +modem +harris +mph +bringing +sick +dose +evaluate +tiffany +tropical +collect +bet +composition +toyota +streets +nationwide +vector +definitely +shaved +turning +buffer +purple +existence +commentary +larry +limousines +developments +def +immigration +destinations +lets +mutual +pipeline +necessarily +syntax +li +attribute +prison +skill +chairs +nl +everyday +apparently +surrounding +mountains +moves +popularity +inquiry +ethernet +checked +exhibit +throw +trend +sierra +visible +cats +desert +postposted +ya +oldest +rhode +nba +coordinator +obviously +mercury +steven +handbook +greg +navigate +worse +summit +victims +epa +spaces +fundamental +burning +escape +coupons +somewhat +receiver +substantial +tr +progressive +cialis +bb +boats +glance +scottish +championship +arcade +richmond +sacramento +impossible +ron +russell +tells +obvious +fiber +depression +graph +covering +platinum +judgment +bedrooms +talks +filing +foster +modeling +passing +awarded +testimonials +trials +tissue +nz +memorabilia +clinton +masters +bonds +cartridge +alberta +explanation +folk +org +commons +cincinnati +subsection +fraud +electricity +permitted +spectrum +arrival +okay +pottery +emphasis +roger +aspect +workplace +awesome +mexican +confirmed +counts +priced +wallpapers +hist +crash +lift +desired +inter +closer +assumes +heights +shadow +riding +infection +firefox +lisa +expense +grove +eligibility +venture +clinic +korean +healing +princess +mall +entering +packet +spray +studios +involvement +dad +buttons +placement +observations +vbulletin +funded +thompson +winners +extend +roads +subsequent +pat +dublin +rolling +fell +motorcycle +yard +disclosure +establishment +memories +nelson +te +arrived +creates +faces +tourist +av +mayor +murder +sean +adequate +senator +yield +presentations +grades +cartoons +pour +digest +reg +lodging +tion +dust +hence +wiki +entirely +replaced +radar +rescue +undergraduate +losses +combat +reducing +stopped +occupation +lakes +donations +associations +citysearch +closely +radiation +diary +seriously +kings +shooting +kent +adds +nsw +ear +flags +pci +baker +launched +elsewhere +pollution +conservative +guestbook +shock +effectiveness +walls +abroad +ebony +tie +ward +drawn +arthur +ian +visited +roof +walker +demonstrate +atmosphere +suggests +kiss +beast +ra +operated +experiment +targets +overseas +purchases +dodge +counsel +federation +pizza +invited +yards +assignment +chemicals +gordon +mod +farmers +rc +queries +bmw +rush +ukraine +absence +nearest +cluster +vendors +mpeg +whereas +yoga +serves +woods +surprise +lamp +rico +partial +shoppers +phil +everybody +couples +nashville +ranking +jokes +cst +http +ceo +simpson +twiki +sublime +counseling +palace +acceptable +satisfied +glad +wins +measurements +verify +globe +trusted +copper +milwaukee +rack +medication +warehouse +shareware +ec +rep +dicke +kerry +receipt +supposed +ordinary +nobody +ghost +violation +configure +stability +mit +applying +southwest +boss +pride +institutional +expectations +independence +knowing +reporter +metabolism +keith +champion +cloudy +linda +ross +personally +chile +anna +plenty +solo +sentence +throat +ignore +maria +uniform +excellence +wealth +tall +rm +somewhere +vacuum +dancing +attributes +recognize +brass +writes +plaza +pdas +outcomes +survival +quest +publish +sri +screening +toe +thumbnail +trans +jonathan +whenever +nova +lifetime +api +pioneer +booty +forgotten +acrobat +plates +acres +venue +athletic +thermal +essays +vital +telling +fairly +coastal +config +cf +charity +intelligent +edinburgh +vt +excel +modes +obligation +campbell +wake +stupid +harbor +hungary +traveler +urw +segment +realize +regardless +lan +enemy +puzzle +rising +aluminum +wells +wishlist +opens +insight +sms +restricted +republican +secrets +lucky +latter +merchants +thick +trailers +repeat +syndrome +philips +attendance +penalty +drum +glasses +enables +nec +iraqi +builder +vista +jessica +chips +terry +flood +foto +ease +arguments +amsterdam +arena +adventures +pupils +stewart +announcement +tabs +outcome +appreciate +expanded +casual +grown +polish +lovely +extras +gm +centres +jerry +clause +smile +lands +ri +troops +indoor +bulgaria +armed +broker +charger +regularly +believed +pine +cooling +tend +gulf +rt +rick +trucks +cp +mechanisms +divorce +laura +shopper +tokyo +partly +nikon +customize +tradition +candy +pills +tiger +donald +folks +sensor +exposed +telecom +hunt +angels +deputy +indicators +sealed +thai +emissions +physicians +loaded +fred +complaint +scenes +experiments +afghanistan +dd +boost +spanking +scholarship +governance +mill +founded +supplements +chronic +icons +moral +den +catering +aud +finger +keeps +pound +locate +camcorder +pl +trained +burn +implementing +roses +labs +ourselves +bread +tobacco +wooden +motors +tough +roberts +incident +gonna +dynamics +lie +crm +rf +conversation +decrease +chest +pension +billy +revenues +emerging +worship +capability +ak +fe +craig +herself +producing +churches +precision +damages +reserves +contributed +solve +shorts +reproduction +minority +td +diverse +amp +ingredients +sb +ah +johnny +sole +franchise +recorder +complaints +facing +sm +nancy +promotions +tones +passion +rehabilitation +maintaining +sight +laid +clay +defence +patches +weak +refund +usc +towns +environments +trembl +divided +blvd +reception +amd +wise +emails +cyprus +wv +odds +correctly +insider +seminars +consequences +makers +hearts +geography +appearing +integrity +worry +ns +discrimination +eve +carter +legacy +marc +pleased +danger +vitamin +widely +processed +phrase +genuine +raising +implications +functionality +paradise +hybrid +reads +roles +intermediate +emotional +sons +leaf +pad +glory +platforms +ja +bigger +billing +diesel +versus +combine +overnight +geographic +exceed +bs +rod +saudi +fault +cuba +hrs +preliminary +districts +introduce +silk +promotional +kate +chevrolet +babies +bi +karen +compiled +romantic +revealed +specialists +generator +albert +examine +jimmy +graham +suspension +bristol +margaret +compaq +sad +correction +wolf +slowly +authentication +communicate +rugby +supplement +showtimes +cal +portions +infant +promoting +sectors +samuel +fluid +grounds +fits +kick +regards +meal +ta +hurt +machinery +bandwidth +unlike +equation +baskets +probability +pot +dimension +wright +img +barry +proven +schedules +admissions +cached +warren +slip +studied +reviewer +involves +quarterly +rpm +profits +devil +grass +comply +marie +florist +illustrated +cherry +continental +alternate +deutsch +achievement +limitations +kenya +webcam +cuts +funeral +nutten +earrings +enjoyed +automated +chapters +pee +charlie +quebec +passenger +convenient +dennis +mars +francis +tvs +sized +manga +noticed +socket +silent +literary +egg +mhz +signals +caps +orientation +pill +theft +childhood +swing +symbols +lat +meta +humans +analog +facial +choosing +talent +dated +flexibility +seeker +wisdom +shoot +boundary +mint +packard +offset +payday +philip +elite +gi +spin +holders +believes +swedish +poems +deadline +jurisdiction +robot +displaying +witness +collins +equipped +stages +encouraged +sur +winds +powder +broadway +acquired +assess +wash +cartridges +stones +entrance +gnome +roots +declaration +losing +attempts +gadgets +noble +glasgow +automation +impacts +rev +gospel +advantages +shore +loves +induced +ll +knight +preparing +loose +aims +recipient +linking +extensions +appeals +cl +earned +illness +islamic +athletics +southeast +ieee +ho +alternatives +pending +parker +determining +lebanon +corp +personalized +kennedy +gt +sh +conditioning +teenage +soap +ae +triple +cooper +nyc +vincent +jam +secured +unusual +answered +partnerships +destruction +slots +increasingly +migration +disorder +routine +toolbar +basically +rocks +conventional +titans +applicants +wearing +axis +sought +genes +mounted +habitat +firewall +median +guns +scanner +herein +occupational +animated +judicial +rio +hs +adjustment +hero +integer +treatments +bachelor +attitude +camcorders +engaged +falling +basics +montreal +carpet +rv +struct +lenses +binary +genetics +attended +difficulty +punk +collective +coalition +pi +dropped +enrollment +duke +walter +ai +pace +besides +wage +producers +ot +collector +arc +hosts +interfaces +advertisers +moments +atlas +strings +dawn +representing +observation +feels +torture +carl +deleted +coat +mitchell +mrs +rica +restoration +convenience +returning +ralph +opposition +container +yr +defendant +warner +confirmation +app +embedded +inkjet +supervisor +wizard +corps +actors +liver +peripherals +liable +brochure +morris +bestsellers +petition +eminem +recall +antenna +picked +assumed +departure +minneapolis +belief +killing +bikini +memphis +shoulder +decor +lookup +texts +harvard +brokers +roy +ion +diameter +ottawa +doll +ic +podcast +seasons +peru +interactions +refine +bidder +singer +evans +herald +literacy +fails +aging +nike +intervention +fed +plugin +attraction +diving +invite +modification +alice +latinas +suppose +customized +reed +involve +moderate +terror +younger +thirty +mice +opposite +understood +rapidly +dealtime +ban +temp +intro +mercedes +zus +assurance +clerk +happening +vast +mills +outline +amendments +tramadol +holland +receives +jeans +metropolitan +compilation +verification +fonts +ent +odd +wrap +refers +mood +favor +veterans +quiz +mx +sigma +gr +attractive +xhtml +occasion +recordings +jefferson +victim +demands +sleeping +careful +ext +beam +gardening +obligations +arrive +orchestra +sunset +tracked +moreover +minimal +polyphonic +lottery +tops +framed +aside +outsourcing +licence +adjustable +allocation +michelle +essay +discipline +amy +ts +demonstrated +dialogue +identifying +alphabetical +camps +declared +dispatched +aaron +handheld +trace +disposal +shut +florists +packs +ge +installing +switches +romania +voluntary +ncaa +thou +consult +phd +greatly +blogging +mask +cycling +midnight +ng +commonly +pe +photographer +inform +turkish +coal +cry +messaging +pentium +quantum +murray +intent +tt +zoo +largely +pleasant +announce +constructed +additions +requiring +spoke +aka +arrow +engagement +sampling +rough +weird +tee +refinance +lion +inspired +holes +weddings +blade +suddenly +oxygen +cookie +meals +canyon +goto +meters +merely +calendars +arrangement +conclusions +passes +bibliography +pointer +compatibility +stretch +durham +furthermore +permits +cooperative +muslim +xl +neil +sleeve +netscape +cleaner +cricket +beef +feeding +stroke +township +rankings +measuring +cad +hats +robin +robinson +jacksonville +strap +headquarters +sharon +crowd +tcp +transfers +surf +olympic +transformation +remained +attachments +dv +dir +entities +customs +administrators +personality +rainbow +hook +roulette +decline +gloves +israeli +medicare +cord +skiing +cloud +facilitate +subscriber +valve +val +hewlett +explains +proceed +flickr +feelings +knife +jamaica +priorities +shelf +bookstore +timing +liked +parenting +adopt +denied +fotos +incredible +britney +freeware +donation +outer +crop +deaths +rivers +commonwealth +pharmaceutical +manhattan +tales +katrina +workforce +islam +nodes +tu +fy +thumbs +seeds +cited +lite +ghz +hub +targeted +organizational +skype +realized +twelve +founder +decade +gamecube +rr +dispute +portuguese +tired +titten +adverse +everywhere +excerpt +eng +steam +discharge +ef +drinks +ace +voices +acute +halloween +climbing +stood +sing +tons +perfume +carol +honest +albany +hazardous +restore +stack +methodology +somebody +sue +ep +housewares +reputation +resistant +democrats +recycling +hang +gbp +curve +creator +amber +qualifications +museums +coding +slideshow +tracker +variation +passage +transferred +trunk +hiking +lb +pierre +jelsoft +headset +photograph +oakland +colombia +waves +camel +distributor +lamps +underlying +hood +wrestling +suicide +archived +photoshop +jp +chi +bt +arabia +gathering +projection +juice +chase +mathematical +logical +sauce +fame +extract +specialized +diagnostic +panama +indianapolis +af +payable +corporations +courtesy +criticism +automobile +confidential +rfc +statutory +accommodations +athens +northeast +downloaded +judges +sl +seo +retired +isp +remarks +detected +decades +paintings +walked +arising +nissan +bracelet +ins +eggs +juvenile +injection +yorkshire +populations +protective +afraid +acoustic +railway +cassette +initially +indicator +pointed +hb +jpg +causing +mistake +norton +locked +eliminate +tc +fusion +mineral +sunglasses +ruby +steering +beads +fortune +preference +canvas +threshold +parish +claimed +screens +cemetery +planner +croatia +flows +stadium +venezuela +exploration +mins +fewer +sequences +coupon +nurses +ssl +stem +proxy +astronomy +lanka +opt +edwards +drew +contests +flu +translate +announces +mlb +costume +tagged +berkeley +voted +killer +bikes +gates +adjusted +rap +tune +bishop +pulled +corn +gp +shaped +compression +seasonal +establishing +farmer +counters +puts +constitutional +grew +perfectly +tin +slave +instantly +cultures +norfolk +coaching +examined +trek +encoding +litigation +submissions +oem +heroes +painted +lycos +ir +zdnet +broadcasting +horizontal +artwork +cosmetic +resulted +portrait +terrorist +informational +ethical +carriers +ecommerce +mobility +floral +builders +ties +struggle +schemes +suffering +neutral +fisher +rat +spears +prospective +bedding +ultimately +joining +heading +equally +artificial +bearing +spectacular +coordination +connector +brad +combo +seniors +worlds +guilty +affiliated +activation +naturally +haven +tablet +jury +dos +tail +subscribers +charm +lawn +violent +mitsubishi +underwear +basin +soup +potentially +ranch +constraints +crossing +inclusive +dimensional +cottage +drunk +considerable +crimes +resolved +mozilla +byte +toner +nose +latex +branches +anymore +oclc +delhi +holdings +alien +locator +selecting +processors +pantyhose +plc +broke +nepal +zimbabwe +difficulties +juan +complexity +msg +constantly +browsing +resolve +barcelona +presidential +documentary +cod +territories +melissa +moscow +thesis +thru +jews +nylon +palestinian +discs +rocky +bargains +frequent +trim +nigeria +ceiling +pixels +ensuring +hispanic +cv +cb +legislature +hospitality +gen +anybody +procurement +diamonds +espn +fleet +untitled +bunch +totals +marriott +singing +theoretical +afford +exercises +starring +referral +nhl +surveillance +optimal +quit +distinct +protocols +lung +highlight +substitute +inclusion +hopefully +brilliant +turner +sucking +cents +reuters +ti +fc +gel +todd +spoken +omega +evaluated +stayed +civic +assignments +fw +manuals +doug +sees +termination +watched +saver +thereof +grill +households +gs +redeem +rogers +grain +aaa +authentic +regime +wanna +wishes +bull +montgomery +architectural +louisville +depend +differ +macintosh +movements +ranging +monica +repairs +breath +amenities +virtually +cole +mart +candle +hanging +colored +authorization +tale +verified +lynn +formerly +projector +bp +situated +comparative +std +seeks +herbal +loving +strictly +routing +docs +stanley +psychological +surprised +retailer +vitamins +elegant +gains +renewal +vid +genealogy +opposed +deemed +scoring +expenditure +brooklyn +liverpool +sisters +critics +connectivity +spots +oo +algorithms +hacker +madrid +similarly +margin +coin +solely +fake +salon +collaborative +norman +fda +excluding +turbo +headed +voters +cure +madonna +commander +arch +ni +murphy +thinks +thats +suggestion +hdtv +soldier +phillips +asin +aimed +justin +bomb +harm +interval +mirrors +spotlight +tricks +reset +brush +investigate +thy +expansys +panels +repeated +assault +connecting +spare +logistics +deer +kodak +tongue +bowling +tri +danish +pal +monkey +proportion +filename +skirt +florence +invest +honey +um +analyzes +drawings +significance +scenario +ye +fs +lovers +atomic +approx +symposium +arabic +gauge +essentials +junction +protecting +nn +faced +mat +rachel +solving +transmitted +weekends +screenshots +produces +oven +ted +intensive +chains +kingston +sixth +engage +deviant +noon +switching +quoted +adapters +correspondence +farms +imports +supervision +cheat +bronze +expenditures +sandy +separation +testimony +suspect +celebrities +macro +sender +mandatory +boundaries +crucial +syndication +gym +celebration +kde +adjacent +filtering +tuition +spouse +exotic +viewer +signup +threats +luxembourg +puzzles +reaching +vb +damaged +cams +receptor +laugh +joel +surgical +destroy +citation +pitch +autos +yo +premises +perry +proved +offensive +imperial +dozen +benjamin +deployment +teeth +cloth +studying +colleagues +stamp +lotus +salmon +olympus +separated +proc +cargo +tan +directive +fx +salem +mate +dl +starter +upgrades +likes +butter +pepper +weapon +luggage +burden +chef +tapes +zones +races +isle +stylish +slim +maple +luke +grocery +offshore +governing +retailers +depot +kenneth +comp +alt +pie +blend +harrison +ls +julie +occasionally +cbs +attending +emission +pete +spec +finest +realty +janet +bow +penn +recruiting +apparent +instructional +phpbb +autumn +traveling +probe +midi +permissions +biotechnology +toilet +ranked +jackets +routes +packed +excited +outreach +helen +mounting +recover +tied +lopez +balanced +prescribed +catherine +timely +talked +debug +delayed +chuck +reproduced +hon +dale +explicit +calculation +villas +ebook +consolidated +exclude +peeing +occasions +brooks +equations +newton +oils +sept +exceptional +anxiety +bingo +whilst +spatial +respondents +unto +lt +ceramic +prompt +precious +minds +annually +considerations +scanners +atm +xanax +eq +pays +fingers +sunny +ebooks +delivers +je +queensland +necklace +musicians +leeds +composite +unavailable +cedar +arranged +lang +theaters +advocacy +raleigh +stud +fold +essentially +designing +threaded +uv +qualify +blair +hopes +assessments +cms +mason +diagram +burns +pumps +footwear +sg +vic +beijing +peoples +victor +mario +pos +attach +licenses +utils +removing +advised +brunswick +spider +phys +ranges +pairs +sensitivity +trails +preservation +hudson +isolated +calgary +interim +assisted +divine +streaming +approve +chose +compound +intensity +technological +syndicate +abortion +dialog +venues +blast +wellness +calcium +newport +antivirus +addressing +pole +discounted +indians +shield +harvest +membrane +prague +previews +bangladesh +constitute +locally +concluded +pickup +desperate +mothers +nascar +iceland +demonstration +governmental +manufactured +candles +graduation +mega +bend +sailing +variations +moms +sacred +addiction +morocco +chrome +tommy +springfield +refused +brake +exterior +greeting +ecology +oliver +congo +glen +botswana +nav +delays +synthesis +olive +undefined +unemployment +cyber +verizon +scored +enhancement +newcastle +clone +velocity +lambda +relay +composed +tears +performances +oasis +baseline +cab +angry +fa +societies +silicon +brazilian +identical +petroleum +compete +ist +norwegian +lover +belong +honolulu +beatles +lips +retention +exchanges +pond +rolls +thomson +barnes +soundtrack +wondering +malta +daddy +lc +ferry +rabbit +profession +seating +dam +cnn +separately +physiology +lil +collecting +das +exports +omaha +tire +participant +scholarships +recreational +dominican +chad +electron +loads +friendship +heather +passport +motel +unions +treasury +warrant +sys +solaris +frozen +occupied +josh +royalty +scales +rally +observer +sunshine +strain +drag +ceremony +somehow +arrested +expanding +provincial +investigations +icq +ripe +yamaha +rely +medications +hebrew +gained +rochester +dying +laundry +stuck +solomon +placing +stops +homework +adjust +assessed +advertiser +enabling +encryption +filling +downloadable +sophisticated +imposed +silence +scsi +focuses +soviet +possession +cu +laboratories +treaty +vocal +trainer +organ +stronger +volumes +advances +vegetables +lemon +toxic +dns +thumbnails +darkness +pty +ws +nuts +nail +bizrate +vienna +implied +span +stanford +sox +stockings +joke +respondent +packing +statute +rejected +satisfy +destroyed +shelter +chapel +gamespot +manufacture +layers +wordpress +guided +vulnerability +accountability +celebrate +accredited +appliance +compressed +bahamas +powell +mixture +bench +univ +tub +rider +scheduling +radius +perspectives +mortality +logging +hampton +christians +borders +therapeutic +pads +butts +inns +bobby +impressive +sheep +accordingly +architect +railroad +lectures +challenging +wines +nursery +harder +cups +ash +microwave +cheapest +accidents +travesti +relocation +stuart +contributors +salvador +ali +salad +np +monroe +tender +violations +foam +temperatures +paste +clouds +competitions +discretion +tft +tanzania +preserve +jvc +poem +unsigned +staying +cosmetics +easter +theories +repository +praise +jeremy +venice +concentrations +estonia +christianity +veteran +streams +landing +signing +executed +katie +negotiations +realistic +dt +cgi +showcase +integral +asks +relax +namibia +generating +christina +congressional +synopsis +hardly +prairie +reunion +composer +bean +sword +absent +photographic +sells +ecuador +hoping +accessed +spirits +modifications +coral +pixel +float +colin +bias +imported +paths +bubble +por +acquire +contrary +millennium +tribune +vessel +acids +focusing +viruses +cheaper +admitted +dairy +admit +mem +fancy +equality +samoa +gc +achieving +tap +stickers +fisheries +exceptions +reactions +leasing +lauren +beliefs +ci +macromedia +companion +squad +analyze +ashley +scroll +relate +divisions +swim +wages +additionally +suffer +forests +fellowship +nano +invalid +concerts +martial +males +victorian +retain +execute +tunnel +genres +cambodia +patents +copyrights +yn +chaos +lithuania +mastercard +wheat +chronicles +obtaining +beaver +updating +distribute +readings +decorative +kijiji +confused +compiler +enlargement +eagles +bases +vii +accused +bee +campaigns +unity +loud +conjunction +bride +rats +defines +airports +instances +indigenous +begun +cfr +brunette +packets +anchor +socks +validation +parade +corruption +stat +trigger +incentives +cholesterol +gathered +essex +slovenia +notified +differential +beaches +folders +dramatic +surfaces +terrible +routers +cruz +pendant +dresses +baptist +scientist +starsmerchant +hiring +clocks +arthritis +bios +females +wallace +nevertheless +reflects +taxation +fever +pmc +cuisine +surely +practitioners +transcript +myspace +theorem +inflation +thee +nb +ruth +pray +stylus +compounds +pope +drums +contracting +arnold +structured +reasonably +jeep +chicks +bare +hung +cattle +mba +radical +graduates +rover +recommends +controlling +treasure +reload +distributors +flame +levitra +tanks +assuming +monetary +elderly +pit +arlington +mono +particles +floating +extraordinary +tile +indicating +bolivia +spell +hottest +stevens +coordinate +kuwait +exclusively +emily +alleged +limitation +widescreen +compile +webster +struck +rx +illustration +plymouth +warnings +construct +apps +inquiries +bridal +annex +mag +gsm +inspiration +tribal +curious +affecting +freight +rebate +meetup +eclipse +sudan +ddr +downloading +rec +shuttle +aggregate +stunning +cycles +affects +forecasts +detect +actively +ciao +ampland +knee +prep +pb +complicated +chem +fastest +butler +shopzilla +injured +decorating +payroll +cookbook +expressions +ton +courier +uploaded +shakespeare +hints +collapse +americas +connectors +unlikely +oe +gif +pros +conflicts +techno +beverage +tribute +wired +elvis +immune +latvia +travelers +forestry +barriers +cant +jd +rarely +gpl +infected +offerings +martha +genesis +barrier +argue +incorrect +trains +metals +bicycle +furnishings +letting +arise +guatemala +celtic +thereby +irc +jamie +particle +perception +minerals +advise +humidity +bottles +boxing +wy +dm +bangkok +renaissance +pathology +sara +bra +ordinance +hughes +photographers +infections +jeffrey +chess +operates +brisbane +configured +survive +oscar +festivals +menus +joan +possibilities +duck +reveal +canal +amino +phi +contributing +herbs +clinics +mls +cow +manitoba +analytical +missions +watson +lying +costumes +strict +dive +saddam +circulation +drill +offense +bryan +cet +protest +assumption +jerusalem +hobby +tries +transexuales +invention +nickname +fiji +technician +inline +executives +enquiries +washing +audi +staffing +cognitive +exploring +trick +enquiry +closure +raid +ppc +timber +volt +intense +div +playlist +registrar +showers +supporters +ruling +steady +dirt +statutes +withdrawal +myers +drops +predicted +wider +saskatchewan +jc +cancellation +plugins +enrolled +sensors +screw +ministers +publicly +hourly +blame +geneva +freebsd +veterinary +acer +prostores +reseller +dist +handed +suffered +intake +informal +relevance +incentive +butterfly +tucson +mechanics +heavily +swingers +fifty +headers +mistakes +numerical +ons +geek +uncle +defining +counting +reflection +sink +accompanied +assure +invitation +devoted +princeton +jacob +sodium +randy +spirituality +hormone +meanwhile +proprietary +timothy +childrens +brick +grip +naval +thumbzilla +medieval +porcelain +avi +bridges +pichunter +captured +watt +thehun +decent +casting +dayton +translated +shortly +cameron +columnists +pins +carlos +reno +donna +andreas +warrior +diploma +cabin +innocent +scanning +ide +consensus +polo +valium +copying +rpg +delivering +cordless +patricia +horn +eddie +uganda +fired +journalism +pd +prot +trivia +adidas +perth +frog +grammar +intention +syria +disagree +klein +harvey +tires +logs +undertaken +tgp +hazard +retro +leo +statewide +semiconductor +gregory +episodes +boolean +circular +anger +diy +mainland +illustrations +suits +chances +interact +snap +happiness +arg +substantially +bizarre +glenn +ur +auckland +olympics +fruits +identifier +geo +ribbon +calculations +doe +jpeg +conducting +startup +suzuki +trinidad +ati +kissing +wal +handy +swap +exempt +crops +reduces +accomplished +calculators +geometry +impression +abs +slovakia +flip +guild +correlation +gorgeous +capitol +sim +dishes +rna +barbados +chrysler +nervous +refuse +extends +fragrance +mcdonald +replica +plumbing +brussels +tribe +neighbors +trades +superb +buzz +transparent +nuke +rid +trinity +charleston +handled +legends +boom +calm +champions +floors +selections +projectors +inappropriate +exhaust +comparing +shanghai +speaks +burton +vocational +davidson +copied +scotia +farming +gibson +pharmacies +fork +troy +ln +roller +introducing +batch +organize +appreciated +alter +nicole +latino +ghana +edges +uc +mixing +handles +skilled +fitted +albuquerque +harmony +distinguished +asthma +projected +assumptions +shareholders +twins +developmental +rip +zope +regulated +triangle +amend +anticipated +oriental +reward +windsor +zambia +completing +gmbh +buf +ld +hydrogen +webshots +sprint +comparable +chick +advocate +sims +confusion +copyrighted +tray +inputs +warranties +genome +escorts +documented +thong +medal +paperbacks +coaches +vessels +walks +sol +keyboards +sage +knives +eco +vulnerable +arrange +artistic +bat +honors +booth +indie +reflected +unified +bones +breed +detector +ignored +polar +fallen +precise +sussex +respiratory +notifications +msgid +transexual +mainstream +invoice +evaluating +lip +subcommittee +sap +gather +suse +maternity +backed +alfred +colonial +mf +carey +motels +forming +embassy +cave +journalists +danny +rebecca +slight +proceeds +indirect +amongst +wool +foundations +msgstr +arrest +volleyball +mw +adipex +horizon +nu +deeply +toolbox +ict +marina +liabilities +prizes +bosnia +browsers +decreased +patio +dp +tolerance +surfing +creativity +lloyd +describing +optics +pursue +lightning +overcome +eyed +ou +quotations +grab +inspector +attract +brighton +beans +bookmarks +ellis +disable +snake +succeed +leonard +lending +oops +reminder +xi +searched +behavioral +riverside +bathrooms +plains +sku +ht +raymond +insights +abilities +initiated +sullivan +za +midwest +karaoke +trap +lonely +fool +ve +nonprofit +lancaster +suspended +hereby +observe +julia +containers +attitudes +karl +berry +collar +simultaneously +racial +integrate +bermuda +amanda +sociology +mobiles +screenshot +exhibitions +kelkoo +confident +retrieved +exhibits +officially +consortium +dies +terrace +bacteria +pts +replied +seafood +novels +rh +rrp +recipients +ought +delicious +traditions +fg +jail +safely +finite +kidney +periodically +fixes +sends +durable +mazda +allied +throws +moisture +hungarian +roster +referring +symantec +spencer +wichita +nasdaq +uruguay +ooo +hz +transform +timer +tablets +tuning +gotten +educators +tyler +futures +vegetable +verse +highs +humanities +independently +wanting +custody +scratch +launches +ipaq +alignment +henderson +bk +britannica +comm +ellen +competitors +nhs +rocket +aye +bullet +towers +racks +lace +nasty +visibility +latitude +consciousness +ste +tumor +ugly +deposits +beverly +mistress +encounter +trustees +watts +duncan +reprints +hart +bernard +resolutions +ment +accessing +forty +tubes +attempted +col +midlands +priest +floyd +ronald +analysts +queue +dx +sk +trance +locale +nicholas +biol +yu +bundle +hammer +invasion +witnesses +runner +rows +administered +notion +sq +skins +mailed +oc +fujitsu +spelling +arctic +exams +rewards +beneath +strengthen +defend +aj +frederick +medicaid +treo +infrared +seventh +gods +une +welsh +belly +aggressive +tex +advertisements +quarters +stolen +cia +sublimedirectory +soonest +haiti +disturbed +determines +sculpture +poly +ears +dod +wp +fist +naturals +neo +motivation +lenders +pharmacology +fitting +fixtures +bloggers +mere +agrees +passengers +quantities +petersburg +consistently +powerpoint +cons +surplus +elder +sonic +obituaries +cheers +dig +taxi +punishment +appreciation +subsequently +om +belarus +nat +zoning +gravity +providence +thumb +restriction +incorporate +backgrounds +treasurer +guitars +essence +flooring +lightweight +ethiopia +tp +mighty +athletes +humanity +transcription +jm +holmes +complications +scholars +dpi +scripting +gis +remembered +galaxy +chester +snapshot +caring +loc +worn +synthetic +shaw +vp +segments +testament +expo +dominant +twist +specifics +itunes +stomach +partially +buried +cn +newbie +minimize +darwin +ranks +wilderness +debut +generations +tournaments +bradley +deny +anatomy +bali +judy +sponsorship +headphones +fraction +trio +proceeding +cube +defects +volkswagen +uncertainty +breakdown +milton +marker +reconstruction +subsidiary +strengths +clarity +rugs +sandra +adelaide +encouraging +furnished +monaco +settled +folding +emirates +terrorists +airfare +comparisons +beneficial +distributions +vaccine +belize +fate +viewpicture +promised +volvo +penny +robust +bookings +threatened +minolta +republicans +discusses +gui +porter +gras +jungle +ver +rn +responded +rim +abstracts +zen +ivory +alpine +dis +prediction +pharmaceuticals +andale +fabulous +remix +alias +thesaurus +individually +battlefield +literally +newer +kay +ecological +spice +oval +implies +cg +soma +ser +cooler +appraisal +consisting +maritime +periodic +submitting +overhead +ascii +prospect +shipment +breeding +citations +geographical +donor +mozambique +tension +href +benz +trash +shapes +wifi +tier +fwd +earl +manor +envelope +diane +homeland +disclaimers +championships +excluded +andrea +breeds +rapids +disco +sheffield +bailey +aus +endif +finishing +emotions +wellington +incoming +prospects +lexmark +cleaners +bulgarian +hwy +eternal +cashiers +guam +cite +aboriginal +remarkable +rotation +nam +preventing +productive +boulevard +eugene +ix +gdp +pig +metric +compliant +minus +penalties +bennett +imagination +hotmail +refurbished +joshua +armenia +varied +grande +closest +activated +actress +mess +conferencing +assign +armstrong +politicians +trackbacks +lit +accommodate +tigers +aurora +una +slides +milan +premiere +lender +villages +shade +chorus +christine +rhythm +digit +argued +dietary +symphony +clarke +sudden +accepting +precipitation +marilyn +lions +findlaw +ada +pools +tb +lyric +claire +isolation +speeds +sustained +matched +approximate +rope +carroll +rational +programmer +fighters +chambers +dump +greetings +inherited +warming +incomplete +vocals +chronicle +fountain +chubby +grave +legitimate +biographies +burner +yrs +foo +investigator +gba +plaintiff +finnish +gentle +bm +prisoners +deeper +muslims +hose +mediterranean +nightlife +footage +howto +worthy +reveals +architects +saints +entrepreneur +carries +sig +freelance +duo +excessive +devon +screensaver +helena +saves +regarded +valuation +unexpected +cigarette +fog +characteristic +marion +lobby +egyptian +tunisia +metallica +outlined +consequently +headline +treating +punch +appointments +str +gotta +cowboy +narrative +bahrain +enormous +karma +consist +betty +queens +academics +pubs +quantitative +lucas +screensavers +subdivision +tribes +vip +defeat +clicks +distinction +honduras +naughty +hazards +insured +harper +livestock +mardi +exemption +tenant +sustainability +cabinets +tattoo +shake +algebra +shadows +holly +formatting +silly +nutritional +yea +mercy +hartford +freely +marcus +sunrise +wrapping +mild +fur +nicaragua +weblogs +timeline +tar +belongs +rj +readily +affiliation +soc +fence +nudist +infinite +diana +ensures +relatives +lindsay +clan +legally +shame +satisfactory +revolutionary +bracelets +sync +civilian +telephony +mesa +fatal +remedy +realtors +breathing +briefly +thickness +adjustments +graphical +genius +discussing +aerospace +fighter +meaningful +flesh +retreat +adapted +barely +wherever +estates +rug +democrat +borough +maintains +failing +shortcuts +ka +retained +voyeurweb +pamela +andrews +marble +extending +jesse +specifies +hull +logitech +surrey +briefing +belkin +dem +accreditation +wav +blackberry +highland +meditation +modular +microphone +macedonia +combining +brandon +instrumental +giants +organizing +shed +balloon +moderators +winston +memo +ham +solved +tide +kazakhstan +hawaiian +standings +partition +invisible +gratuit +consoles +funk +fbi +qatar +magnet +translations +porsche +cayman +jaguar +reel +sheer +commodity +posing +kilometers +rp +bind +thanksgiving +rand +hopkins +urgent +guarantees +infants +gothic +cylinder +witch +buck +indication +eh +congratulations +tba +cohen +sie +usgs +puppy +kathy +acre +graphs +surround +cigarettes +revenge +expires +enemies +lows +controllers +aqua +chen +emma +consultancy +finances +accepts +enjoying +conventions +eva +patrol +smell +pest +hc +italiano +coordinates +rca +fp +carnival +roughly +sticker +promises +responding +reef +physically +divide +stakeholders +hydrocodone +gst +consecutive +cornell +satin +bon +deserve +attempting +mailto +promo +jj +representations +chan +worried +tunes +garbage +competing +combines +mas +beth +bradford +len +phrases +kai +peninsula +chelsea +boring +reynolds +dom +jill +accurately +speeches +reaches +schema +considers +sofa +catalogs +ministries +vacancies +quizzes +parliamentary +obj +prefix +lucia +savannah +barrel +typing +nerve +dans +planets +deficit +boulder +pointing +renew +coupled +viii +myanmar +metadata +harold +circuits +floppy +texture +handbags +jar +ev +somerset +incurred +acknowledge +thoroughly +antigua +nottingham +thunder +tent +caution +identifies +questionnaire +qualification +locks +modelling +namely +miniature +dept +hack +dare +euros +interstate +pirates +aerial +hawk +consequence +rebel +systematic +perceived +origins +hired +makeup +textile +lamb +madagascar +nathan +tobago +presenting +cos +troubleshooting +uzbekistan +indexes +pac +rl +erp +centuries +gl +magnitude +ui +richardson +hindu +dh +fragrances +vocabulary +licking +earthquake +vpn +fundraising +fcc +markers +weights +albania +geological +assessing +lasting +wicked +eds +introduces +kills +roommate +webcams +pushed +webmasters +ro +df +computational +acdbentity +participated +junk +handhelds +wax +lucy +answering +hans +impressed +slope +reggae +failures +poet +conspiracy +surname +theology +nails +evident +whats +rides +rehab +epic +saturn +organizer +nut +allergy +sake +twisted +combinations +preceding +merit +enzyme +cumulative +zshops +planes +edmonton +tackle +disks +condo +pokemon +amplifier +ambien +arbitrary +prominent +retrieve +lexington +vernon +sans +worldcat +titanium +irs +fairy +builds +contacted +shaft +lean +bye +cdt +recorders +occasional +leslie +casio +deutsche +ana +postings +innovations +kitty +postcards +dude +drain +monte +fires +algeria +blessed +luis +reviewing +cardiff +cornwall +favors +potato +panic +explicitly +sticks +leone +transsexual +ez +citizenship +excuse +reforms +basement +onion +strand +pf +sandwich +uw +lawsuit +alto +informative +girlfriend +bloomberg +cheque +hierarchy +influenced +banners +reject +eau +abandoned +bd +circles +italic +beats +merry +mil +scuba +gore +complement +cult +dash +passive +mauritius +valued +cage +checklist +requesting +courage +verde +lauderdale +scenarios +gazette +hitachi +divx +extraction +batman +elevation +hearings +coleman +hugh +lap +utilization +beverages +calibration +jake +eval +efficiently +anaheim +ping +textbook +dried +entertaining +prerequisite +luther +frontier +settle +stopping +refugees +knights +hypothesis +palmer +medicines +flux +derby +sao +peaceful +altered +pontiac +regression +doctrine +scenic +trainers +muze +enhancements +renewable +intersection +passwords +sewing +consistency +collectors +conclude +munich +oman +celebs +gmc +propose +hh +azerbaijan +lighter +rage +adsl +uh +prix +astrology +advisors +pavilion +tactics +trusts +occurring +supplemental +travelling +talented +annie +pillow +induction +derek +precisely +shorter +harley +spreading +provinces +relying +finals +paraguay +steal +parcel +refined +fd +bo +fifteen +widespread +incidence +fears +predict +boutique +acrylic +rolled +tuner +avon +incidents +peterson +rays +asn +shannon +toddler +enhancing +flavor +alike +walt +homeless +horrible +hungry +metallic +acne +blocked +interference +warriors +palestine +listprice +libs +undo +cadillac +atmospheric +malawi +wm +pk +sagem +knowledgestorm +dana +halo +ppm +curtis +parental +referenced +strikes +lesser +publicity +marathon +ant +proposition +gays +pressing +gasoline +apt +dressed +scout +belfast +exec +dealt +niagara +inf +eos +warcraft +charms +catalyst +trader +bucks +allowance +vcr +denial +uri +designation +thrown +prepaid +raises +gem +duplicate +electro +criterion +badge +wrist +civilization +analyzed +vietnamese +heath +tremendous +ballot +lexus +varying +remedies +validity +trustee +maui +weighted +angola +performs +plastics +realm +corrected +jenny +helmet +salaries +postcard +elephant +yemen +encountered +tsunami +scholar +nickel +internationally +surrounded +psi +buses +expedia +geology +pct +wb +creatures +coating +commented +wallet +cleared +smilies +vids +accomplish +boating +drainage +shakira +corners +broader +vegetarian +rouge +yeast +yale +newfoundland +sn +qld +pas +clearing +investigated +dk +ambassador +coated +intend +stephanie +contacting +vegetation +doom +findarticles +louise +kenny +specially +owen +routines +hitting +yukon +beings +bite +issn +aquatic +reliance +habits +striking +myth +infectious +podcasts +singh +gig +gilbert +sas +ferrari +continuity +brook +fu +outputs +phenomenon +ensemble +insulin +assured +biblical +weed +conscious +accent +mysimon +eleven +wives +ambient +utilize +mileage +oecd +prostate +adaptor +auburn +unlock +hyundai +pledge +vampire +angela +relates +nitrogen +xerox +dice +merger +softball +referrals +quad +dock +differently +firewire +mods +nextel +framing +musician +blocking +rwanda +sorts +integrating +vsnet +limiting +dispatch +revisions +papua +restored +hint +armor +riders +chargers +remark +dozens +varies +msie +reasoning +wn +liz +rendered +picking +charitable +guards +annotated +ccd +sv +convinced +openings +buys +burlington +replacing +researcher +watershed +councils +occupations +acknowledged +kruger +pockets +granny +pork +zu +equilibrium +viral +inquire +pipes +characterized +laden +aruba +cottages +realtor +merge +privilege +edgar +develops +qualifying +chassis +dubai +estimation +barn +pushing +llp +fleece +pediatric +boc +fare +dg +asus +pierce +allan +dressing +techrepublic +sperm +vg +bald +filme +craps +fuji +frost +leon +institutes +mold +dame +fo +sally +yacht +tracy +prefers +drilling +brochures +herb +tmp +alot +ate +breach +whale +traveller +appropriations +suspected +tomatoes +benchmark +beginners +instructors +highlighted +bedford +stationery +idle +mustang +unauthorized +clusters +antibody +competent +momentum +fin +wiring +io +pastor +mud +calvin +uni +shark +contributor +demonstrates +phases +grateful +emerald +gradually +laughing +grows +cliff +desirable +tract +ul +ballet +ol +journalist +abraham +js +bumper +afterwards +webpage +religions +garlic +hostels +shine +senegal +explosion +pn +banned +wendy +briefs +signatures +diffs +cove +mumbai +ozone +disciplines +casa +mu +daughters +conversations +radios +tariff +nvidia +opponent +pasta +simplified +muscles +serum +wrapped +swift +motherboard +runtime +inbox +focal +bibliographic +eden +distant +incl +champagne +ala +decimal +hq +deviation +superintendent +propecia +dip +nbc +samba +hostel +housewives +employ +mongolia +penguin +magical +influences +inspections +irrigation +miracle +manually +reprint +reid +wt +hydraulic +centered +robertson +flex +yearly +penetration +wound +belle +rosa +conviction +hash +omissions +writings +hamburg +lazy +mv +mpg +retrieval +qualities +cindy +fathers +carb +charging +cas +marvel +lined +cio +dow +prototype +importantly +rb +petite +apparatus +upc +terrain +dui +pens +explaining +yen +strips +gossip +rangers +nomination +empirical +mh +rotary +worm +dependence +discrete +beginner +boxed +lid +sexuality +polyester +cubic +deaf +commitments +suggesting +sapphire +kinase +skirts +mats +remainder +crawford +labeled +privileges +televisions +specializing +marking +commodities +pvc +serbia +sheriff +griffin +declined +guyana +spies +blah +mime +neighbor +motorcycles +elect +highways +thinkpad +concentrate +intimate +reproductive +preston +deadly +feof +bunny +chevy +molecules +rounds +longest +refrigerator +tions +intervals +sentences +dentists +usda +exclusion +workstation +holocaust +keen +flyer +peas +dosage +receivers +urls +disposition +variance +navigator +investigators +cameroon +baking +marijuana +adaptive +computed +needle +baths +enb +gg +cathedral +brakes +og +nirvana +ko +fairfield +owns +til +invision +sticky +destiny +generous +madness +emacs +climb +blowing +fascinating +landscapes +heated +lafayette +jackie +wto +computation +hay +cardiovascular +ww +sparc +cardiac +salvation +dover +adrian +predictions +accompanying +vatican +brutal +learners +gd +selective +arbitration +configuring +token +editorials +zinc +sacrifice +seekers +guru +isa +removable +convergence +yields +gibraltar +levy +suited +numeric +anthropology +skating +kinda +aberdeen +emperor +grad +malpractice +dylan +bras +belts +blacks +educated +rebates +reporters +burke +proudly +pix +necessity +rendering +mic +inserted +pulling +basename +kyle +obesity +curves +suburban +touring +clara +vertex +bw +hepatitis +nationally +tomato +andorra +waterproof +expired +mj +travels +flush +waiver +pale +specialties +hayes +humanitarian +invitations +functioning +delight +survivor +garcia +cingular +economies +alexandria +bacterial +moses +counted +undertake +declare +continuously +johns +valves +gaps +impaired +achievements +donors +tear +jewel +teddy +lf +convertible +ata +teaches +ventures +nil +bufing +stranger +tragedy +julian +nest +pam +dryer +painful +velvet +tribunal +ruled +nato +pensions +prayers +funky +secretariat +nowhere +cop +paragraphs +gale +joins +adolescent +nominations +wesley +dim +lately +cancelled +scary +mattress +mpegs +brunei +likewise +banana +introductory +slovak +cakes +stan +reservoir +occurrence +idol +mixer +remind +wc +worcester +sbjct +demographic +charming +mai +tooth +disciplinary +annoying +respected +stays +disclose +affair +drove +washer +upset +restrict +springer +beside +mines +portraits +rebound +logan +mentor +interpreted +evaluations +fought +baghdad +elimination +metres +hypothetical +immigrants +complimentary +helicopter +pencil +freeze +hk +performer +abu +titled +commissions +sphere +powerseller +moss +ratios +concord +graduated +endorsed +ty +surprising +walnut +lance +ladder +italia +unnecessary +dramatically +liberia +sherman +cork +maximize +cj +hansen +senators +workout +mali +yugoslavia +bleeding +characterization +colon +likelihood +lanes +purse +fundamentals +contamination +mtv +endangered +compromise +optimize +stating +dome +caroline +leu +expiration +namespace +align +peripheral +bless +engaging +negotiation +crest +opponents +triumph +nominated +confidentiality +electoral +changelog +welding +deferred +alternatively +heel +alloy +condos +plots +polished +yang +gently +greensboro +tulsa +locking +casey +controversial +draws +fridge +blanket +bloom +qc +simpsons +lou +elliott +recovered +fraser +justify +upgrading +blades +pgp +loops +surge +frontpage +trauma +aw +tahoe +advert +possess +demanding +defensive +sip +flashers +subaru +forbidden +tf +vanilla +programmers +pj +monitored +installations +deutschland +picnic +souls +arrivals +spank +cw +practitioner +motivated +wr +dumb +smithsonian +hollow +vault +securely +examining +fioricet +groove +revelation +rg +pursuit +delegation +wires +bl +dictionaries +mails +backing +greenhouse +sleeps +vc +blake +transparency +dee +travis +wx +endless +figured +orbit +currencies +niger +bacon +survivors +positioning +heater +colony +cannon +circus +promoted +forbes +mae +moldova +mel +descending +paxil +spine +trout +enclosed +feat +temporarily +ntsc +cooked +thriller +transmit +apnic +fatty +gerald +pressed +frequencies +scanned +reflections +hunger +mariah +sic +municipality +usps +joyce +detective +surgeon +cement +experiencing +fireplace +endorsement +bg +planners +disputes +textiles +missile +intranet +closes +seq +psychiatry +persistent +deborah +conf +marco +assists +summaries +glow +gabriel +auditor +wma +aquarium +violin +prophet +cir +bracket +looksmart +isaac +oxide +oaks +magnificent +erik +colleague +naples +promptly +modems +adaptation +hu +harmful +paintball +prozac +sexually +enclosure +acm +dividend +newark +kw +paso +glucose +phantom +norm +playback +supervisors +westminster +turtle +ips +distances +absorption +treasures +dsc +warned +neural +ware +fossil +mia +hometown +badly +transcripts +apollo +wan +disappointed +persian +continually +communist +collectible +handmade +greene +entrepreneurs +robots +grenada +creations +jade +scoop +acquisitions +foul +keno +gtk +earning +mailman +sanyo +nested +biodiversity +excitement +somalia +movers +verbal +blink +presently +seas +carlo +workflow +mysterious +novelty +bryant +tiles +voyuer +librarian +subsidiaries +switched +stockholm +tamil +garmin +ru +pose +fuzzy +indonesian +grams +therapist +richards +mrna +budgets +toolkit +promising +relaxation +goat +render +carmen +ira +sen +thereafter +hardwood +erotica +temporal +sail +forge +commissioners +dense +dts +brave +forwarding +qt +awful +nightmare +airplane +reductions +southampton +istanbul +impose +organisms +sega +telescope +viewers +asbestos +portsmouth +cdna +meyer +enters +pod +savage +advancement +wu +harassment +willow +resumes +bolt +gage +throwing +existed +generators +lu +wagon +barbie +dat +soa +knock +urge +smtp +generates +potatoes +thorough +replication +inexpensive +kurt +receptors +peers +roland +optimum +neon +interventions +quilt +huntington +creature +ours +mounts +syracuse +internship +lone +refresh +aluminium +snowboard +beastality +webcast +michel +evanescence +subtle +coordinated +notre +shipments +maldives +stripes +firmware +antarctica +cope +shepherd +lm +canberra +cradle +chancellor +mambo +lime +kirk +flour +controversy +legendary +bool +sympathy +choir +avoiding +beautifully +blond +expects +cho +jumping +fabrics +antibodies +polymer +hygiene +wit +poultry +virtue +burst +examinations +surgeons +bouquet +immunology +promotes +mandate +wiley +departmental +bbs +spas +ind +corpus +johnston +terminology +gentleman +fibre +reproduce +convicted +shades +jets +indices +roommates +adware +qui +intl +threatening +spokesman +zoloft +activists +frankfurt +prisoner +daisy +halifax +encourages +ultram +cursor +assembled +earliest +donated +stuffed +restructuring +insects +terminals +crude +morrison +maiden +simulations +cz +sufficiently +examines +viking +myrtle +bored +cleanup +yarn +knit +conditional +mug +crossword +bother +budapest +conceptual +knitting +attacked +hl +bhutan +liechtenstein +mating +compute +redhead +arrives +translator +automobiles +tractor +allah +continent +ob +unwrap +fares +longitude +resist +challenged +telecharger +hoped +pike +safer +insertion +instrumentation +ids +hugo +wagner +constraint +groundwater +touched +strengthening +cologne +gzip +wishing +ranger +smallest +insulation +newman +marsh +ricky +ctrl +scared +theta +infringement +bent +laos +subjective +monsters +asylum +lightbox +robbie +stake +cocktail +outlets +swaziland +varieties +arbor +mediawiki +configurations +poison +y +a +haber +como +estar +tener +pero +porque +este +ese +todo +desde +sobre +tambien +hasta +donde +quien +cuando +muy +entre +otro +despues +mucho +deber +entonces +primer +tiempo +mismo +ahora +cada +tanto +durante +nuevo +algun +menos +decir +poco +antes +mundo +vida +dia +hombre +mujer +rey +reina +paz +agua +perro +gato +manzana +computadora +pais +ciudad +trabajo +persona +familia +hijo +hija +padre +madre +hermano +hermana +amigo +amiga +amor +odio +guerra +libro +luna +estrella +cielo +tierra +arbol +flor +comida +vino +leche +carne +pescado +fruta +verdura +rojo +azul +amarillo +blanco +negro +pequeno +bajo +bueno +malo +feliz +triste +rapido +lento +caliente +frio +viejo +joven +fuerte +debil +pobre +verdad +mentira +pregunta +respuesta +problema +solucion +mente +cuerpo +cabeza +mano +ojo +boca +corazon +sangre +voz +musica +arte +ciencia +historia +futuro +pasado +presente +numero +palabra +idioma +espanol +ingles +frances +puerta +ventana +silla +cama +escuela +universidad +iglesia +tienda +mercado +dinero +precio +compra +venta +coche +autobus +tren +avion +barco +camino +calle +parque +bosque +playa +isla +norte +oeste +izquierda +derecha +arriba +abajo +dentro +fuera +cerca +lejos +hoy +ayer +manana +noche +tarde +hora +minuto +segundo +ano +mes +semana +enero +febrero +marzo +abril +mayo +junio +julio +agosto +lunes +martes +miercoles +jueves +viernes +sabado +domingo +uno +tres +cuatro +cinco +seis +siete +ocho +nueve +diez +cien +primero +ultimo +mejor +peor +nada +algo +alguien +nadie +siempre +nunca +talvez +quizas +claro +oscuro +libre +ocupado +abierto +cerrado +posible +imposible +importante +dificil +facil +necesario +especial +comun +raro +bonito +feo +largo +corto +ancho +estrecho +dulce +amargo +salado +suave +duro +limpio +sucio +seguro +peligroso +tranquilo +ruido +silencio +luz +sombra +fuego +hielo +viento +lluvia +nieve +nube +rayo +trueno +naturaleza +planta +piedra +madera +papel +plastico +vidrio +ropa +zapato +sombrero +bolsa +caja +llave +telefono +pantalla +teclado +raton +programa +dato +archivo +usuario +sistema +servidor +cliente +mensaje +correo +carta +noticia +periodico +revista +pelicula +teatro +deporte +juego +partido +equipo +jugador +ganador +perdedor +derrota +objetivo +exito +fracaso +esfuerzo +ayuda +apoyo +regalo +fiesta +viaje +vacaciones +restaurante +bebida +salud +enfermedad +medico +medicina +dolor +alegria +miedo +esperanza +sueno +realidad +recuerdo +olvido +nombre +apellido +edad +forma +tamano +peso +velocidad +fuerza +poder +derecho +ley +gobierno +pueblo +nacion +universo +dios +alma +espiritu +cultura +tradicion +costumbre +moda +estilo +diseno +pintura +escultura +camara +cine +escena +actriz +autor +poeta +cancion +baile +instrumento +guitarra +tambor +nota +ritmo +melodia +armonia +sonido +volumen +tono +grito +susurro +risa +llanto +sonrisa +lagrima +beso +abrazo +dedo +brazo +pierna +rodilla +hombro +espalda +pecho +estomago +piel +pelo +uñas +diente +lengua +nariz +oreja +cuello +cintura +cadera +tobillo +talon +carrera +salto +golpe +caida +subida +bajada +entrada +salida +inicio +principio +centro +borde +lado +parte +mitad +cuarto +tercio +doble +unico +varios +muchos +pocos +bastante +demasiado +suficiente +util +inutil +probable +cierto +falso +humano +vegetal +solido +liquido +gaseoso +calido +fresco +humedo +seco +lleno +vacio +pesado +ligero +grueso +fino +brillante +opaco +transparente +conocido +desconocido +famoso +anonimo +publico +privado +oficial +nacional +internacional +moderno +antiguo +temprano +pronto +luego +aqui +alli +ahi +adonde +cual +cuanto +cuanta +cuantos +cuantas +esta +estos +estas +esa +esos +esas +aquel +aquella +aquellos +aquellas +mio +tuyo +tuya +suyo +suya +nuestro +nuestra +vuestro +vuestra diff --git a/public/vocab_es.txt b/public/vocab_es.txt new file mode 100644 index 0000000000000000000000000000000000000000..fca67834de8ebeea84a72b58aea08c2d6c7680cc --- /dev/null +++ b/public/vocab_es.txt @@ -0,0 +1,600 @@ +# Spanish seed vocabulary for EN∪ES merge (multillm Slice 4). +# Includes roadmap §15 demo translations + high-frequency nouns/adjectives. +# Languages beyond ES are out of scope for this epic. +el +la +de +que +y +a +en +un +ser +se +no +haber +por +con +su +para +como +estar +tener +le +yo +pero +porque +este +ese +todo +desde +sobre +tambien +me +hasta +hay +donde +quien +cuando +muy +sin +sobre +entre +si +dos +otro +despues +mucho +deber +entonces +primer +grande +tiempo +mismo +ese +ahora +cada +tanto +durante +nuevo +algun +menos +decir +mayor +poco +antes +mundo +vida +dia +hombre +mujer +rey +reina +paz +agua +perro +gato +manzana +computadora +casa +pais +ciudad +trabajo +persona +familia +hijo +hija +padre +madre +hermano +hermana +amigo +amiga +amor +odio +guerra +libro +sol +luna +estrella +cielo +tierra +mar +rio +montana +arbol +flor +animal +comida +pan +vino +leche +cafe +te +carne +pescado +fruta +verdura +rojo +azul +verde +amarillo +blanco +negro +grande +pequeno +alto +bajo +bueno +malo +feliz +triste +rapido +lento +caliente +frio +nuevo +viejo +joven +viejo +fuerte +debil +rico +pobre +verdad +mentira +pregunta +respuesta +problema +solucion +idea +mente +cuerpo +cabeza +mano +pie +ojo +boca +corazon +sangre +voz +musica +arte +ciencia +historia +futuro +pasado +presente +numero +palabra +idioma +espanol +ingles +frances +puerta +ventana +mesa +silla +cama +escuela +universidad +hospital +iglesia +tienda +mercado +dinero +precio +compra +venta +auto +coche +autobus +tren +avion +barco +camino +calle +plaza +parque +bosque +playa +isla +norte +sur +este +oeste +norte +izquierda +derecha +arriba +abajo +dentro +fuera +cerca +lejos +hoy +ayer +manana +noche +tarde +hora +minuto +segundo +ano +mes +semana +enero +febrero +marzo +abril +mayo +junio +julio +agosto +lunes +martes +miercoles +jueves +viernes +sabado +domingo +uno +dos +tres +cuatro +cinco +seis +siete +ocho +nueve +diez +cien +mil +primero +segundo +ultimo +mejor +peor +mismo +otro +nada +algo +alguien +nadie +siempre +nunca +talvez +quizas +claro +oscuro +libre +ocupado +abierto +cerrado +posible +imposible +importante +simple +dificil +facil +necesario +especial +comun +raro +bonito +feo +largo +corto +ancho +estrecho +dulce +amargo +salado +suave +duro +limpio +sucio +seguro +peligroso +tranquilo +ruido +silencio +luz +sombra +fuego +hielo +viento +lluvia +nieve +nube +rayo +trueno +naturaleza +animal +planta +piedra +metal +madera +papel +plastico +vidrio +ropa +zapato +sombrero +bolsa +caja +llave +telefono +pantalla +teclado +raton +internet +programa +dato +archivo +usuario +sistema +red +servidor +cliente +mensaje +correo +carta +noticia +periodico +revista +pelicula +teatro +deporte +juego +partido +equipo +jugador +ganador +perdedor +victoria +derrota +meta +objetivo +exito +fracaso +esfuerzo +ayuda +apoyo +regalo +fiesta +viaje +vacaciones +hotel +restaurante +comida +bebida +salud +enfermedad +medico +medicina +dolor +alegria +miedo +esperanza +sueno +realidad +recuerdo +olvido +nombre +apellido +edad +color +forma +tamano +peso +velocidad +fuerza +poder +derecho +deber +ley +gobierno +pueblo +nacion +mundo +universo +dios +alma +espiritu +fe +religion +cultura +tradicion +costumbre +moda +estilo +diseno +pintura +escultura +foto +camara +radio +television +cine +escena +actor +actriz +director +autor +poeta +cancion +baile +instrumento +guitarra +piano +violin +tambor +nota +ritmo +melodia +armonia +ruido +eco +sonido +volumen +tono +voz +grito +susurro +risa +llanto +sonrisa +lagrima +beso +abrazo +mano +dedo +brazo +pierna +rodilla +hombro +espalda +pecho +estomago +piel +pelo +uñas +diente +lengua +nariz +oreja +cuello +cintura +cadera +tobillo +talon +paso +carrera +salto +golpe +caida +subida +bajada +entrada +salida +inicio +fin +principio +final +centro +borde +lado +parte +todo +nada +mitad +cuarto +tercio +doble +triple +unico +varios +muchos +pocos +bastante +demasiado +suficiente +necesario +util +inutil +posible +probable +cierto +falso +real +virtual +natural +artificial +humano +animal +vegetal +mineral +solido +liquido +gaseoso +calido +fresco +humedo +seco +lleno +vacio +pesado +ligero +grueso +fino +brillante +opaco +transparente +opaco +visible +invisible +conocido +desconocido +famoso +anonimo +publico +privado +oficial +personal +general +particular +local +global +nacional +internacional +moderno +antiguo +actual +futuro +pasado +presente +temprano +tarde +pronto +luego +despues +antes +ahora +entonces +aqui +alli +ahi +donde +adonde +como +porque +cuando +quien +cual +cuanto +cuanta +cuantos +cuantas +este +esta +estos +estas +ese +esa +esos +esas +aquel +aquella +aquellos +aquellas +mio +mia +tuyo +tuya +suyo +suya +nuestro +nuestra +vuestro +vuestra diff --git a/roadmap/HANDOFF-shared-noise.md b/roadmap/HANDOFF-shared-noise.md new file mode 100644 index 0000000000000000000000000000000000000000..821b57a30992b7b98edaaa0e1e7642e6b6d122b1 --- /dev/null +++ b/roadmap/HANDOFF-shared-noise.md @@ -0,0 +1,109 @@ +# Handoff — Shared noise / Zero coverage (2026-08-26) + +Pegá esto en el chatbot nuevo. Pedile que lea también: + +- `.agents/Por acá va la bocha.md` (estilo) +- `roadmap/shared-noise-omit-common.md` (fuente de verdad del acuerdo, draft) +- `current-research/DISCOVERY-shared-noise-embedding-geometry.md` (evidencia Arctic@256 vs mpnet@768 — knob “muerto” ≠ wiring roto) +- `.agents/skills/dev-protocol/lessons-learned.md` §8.9 +- **No implementar** hasta que el humano lo pida explícito. + +Repo: `vhectorlab`. Branch de trabajo al corte: `main` @ producto **3.0.0**. UI: COMPARE / ANALYSIS / POINTS, chrome Workbench. + +--- + +## Qué se pidió al inicio + +Auditar commits recientes de **Zero coverage** y **Shared noise**. Sospecha de regresión. Ser crítico: podía ser percepción. + +## Qué hay en `main` (hechos) + +Línea shipped: + +1. **8 ago `af73265`:** Shared noise = medias **G1↔G2**. Zero coverage On/Off + piso 30%. +2. **26 ago `3564719` / 2.4.3:** Shared noise = **min/max de todos los tokens** (`groupId` ignorado). Techo coverage **100%**. Paint hacia negro. Un outlier **veta** toda la dim (D4). +3. **Knobs A/mA + 3.0.0:** misma matemática; drag DAW; mA 5 decimales. No toca el motor de contraste. + +Tests verdes al auditar: `groupDimContrast` / `coverageAmKnobs` / `visualizationControls`. + +**No está en `main`:** rama `feat/shared-noise-per-word` (`d47e370`, `258361d`) — agujeros per-word sobre `t` normalizado. Nunca mergeó. + +**2.4.3 no es un bug de merge.** Es un retarget. En el bootstrap Compare (~3 grupos, muchos tokens) min/max casi no dispara → el knob “no hace nada”. Zero coverage 30% sí se ve (low-cut sobre `|t|`). Las dos fórmulas se llaman Coverage/Similarity pero **no miden lo mismo**. + +Paint-only: el palito **sigue ondulando**. El humano después dijo que eso **no** sale de la visión. + +Captura típica del síntoma: COMPARE + ANALYSIS + POINTS, **SAE ON**, n≈230, Zero coverage ~61%, Shared noise Similarity ~77%, franjas **amarillas** cruzando grupos. Eso es z-score de features SAE compartidas y grandes, no “Shared noise roto en el shader”. + +--- + +## Hipótesis de producto (texto original del humano) + +Dentro de cada modelo hay dims con valores muy parecidos **cualquier token**. ON/OFF + control (antes slide, ahora **knobs**): ir de **menos a más** — primero ocultar número y signo más parecidos, después más, hasta que queden las dims que diferencian tokens. Después, leer patrones semánticos **dentro de grupos**. Grupos = lectura, no la métrica. + +**Vocabulario:** **ruido** = lo **compartido** (el pack). El humano se corrigió: no llamar ruido a lo distintivo. + +Cada token = un thread. Cada punto = float de esa dim. Y = valor. Color = rampa +1 amarillo / 0 negro / −1 violeta. + +--- + +## Decisiones cerradas (no reabrir) + +| ID | Qué | +| :--- | :--- | +| L1 | Unidad = **punto** (token × dim). No borrar la columna entera. `groupId` no decide el hide. | +| L2 | **229 iguales + 1 distinto:** ese **1 queda**. El pack se oculta con el knob. | +| L3 | Pack: **fuera de la visión** (geometría, no solo paint). X **no** se compacta. El punto distinto **conserva** Y y color. | +| L4 | Gradual milimétrico: umbral de “parecido” sobre el **float nativo** visualizado (float32 / lo que dé el modelo). No ε de colormap `0.01`. No un % grosero que tire dígitos. | +| L5 | UI: **Shared noise** / **Similarity**. OFF = no oculta. | +| L6 | **Zero coverage** = colormap (banda del 0). Otro control. | +| L7 | Sign conflict / Group hue = lectura. | +| L8 | **SAE Top‑K de este lab es la capa equivocada** para esta cacería. Primera lectura en **RAW**, SAE OFF. No “arreglar” entrenando más el SAE. | + +**Anulado (draft intermedio):** omitir la **columna completa** para todos los tokens. Eso mataba al único punto de interés. + +**Cercano en código, no es copy-paste:** `feat/shared-noise-per-word` (stick `t`, agujeros duros). Acá: float nativo + hide gradual del pack. + +--- + +## SAE (para no mezclar instrumentos) + +Lab SAE = diccionario entrenado en el **batch actual** + Top‑K **más grandes** por token para **reconstruir**. No es “mostrá lo único vs el resto”. Lo común y grande sobrevive. Lo único y chico el K lo tira. Las franjas amarillas = diccionario útil, no wiring roto. + +Cazar “este float32 difiere en dim k” → embedding **RAW**. Un SAE tipo IDF / penalizar latents frecuentes sería **otro epic**. + +--- + +## Abierto + +**Dos packs sin un “uno”:** 50 tokens a `+0.8` y 50 a `−0.7`. Si “se parece a alguien → se oculta”, **los dos bloques se van** y no ves el concepto de grupo. Si el humano todavía quiere ver bloques de grupo, hace falta otra regla (Keep / solo sacar la pared global). **No cerrado.** El caso 229+1 **sí**. + +Opcional a confirmar: primer demo = COMPARE + RAW + SAE OFF. + +--- + +## Qué no hacer + +- No implementar todavía. +- No volver Shared noise a medias G1↔G2. +- No “arreglar” con más precisión de knobs A/mA solos (3.0.0): no ocultan geometría del pack. +- No compactar X. +- No tratar Zero coverage y Shared noise como el mismo gesto. + +--- + +## Archivos + +| Path | Rol | +| :--- | :--- | +| `roadmap/shared-noise-omit-common.md` | Acuerdo vivo | +| `roadmap/open-shared-noise-knob.md` | Parking knob-feel; superseded | +| `roadmap/archivo/shared-noise-coverage-knobs.md` | 2.4.3 paint + token-batch | +| `src/visualizer/groupDimContrast.js` | Métrica/paint actuales | +| `src/ui/coverageAmKnobs.js` | Knobs A/mA | +| `src/ui/visualizationControlsDefaults.js` | Zero coverage remap, persist `vl3d.viz.*` | + +--- + +## Estado de la conversación + +Hablando. Roadmap actualizado. El humano puede seguir en otro chat desde este handoff + `shared-noise-omit-common.md`. Siguiente útil: cerrar el caso dos packs, o pedir implementación (dev-protocol, rama, TDD) **después** de OK explícito. diff --git a/roadmap/README.md b/roadmap/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6d15c43f83609698dff805117358a23fdcb18021 --- /dev/null +++ b/roadmap/README.md @@ -0,0 +1,18 @@ +# Roadmap + +Documentos de planificación **activos** viven en esta carpeta. Históricos en **[`archivo/`](./archivo/)**. + +| Doc | Tema | Estado | +|---|---|---| +| [`multillm/`](./multillm/) | Multi-embedding catalog + profiles + `setup.sh` option 11 (local; HF deferred) | **Activo (plan)** | +| [`HANDOFF-shared-noise.md`](./HANDOFF-shared-noise.md) | Handoff 2026-08-26 (auditoría + acuerdo Shared noise) | **Para pegar en otro chat** | +| [`../current-research/DISCOVERY-shared-noise-embedding-geometry.md`](../current-research/DISCOVERY-shared-noise-embedding-geometry.md) | Evidencia: Shared noise vivo en Arctic@256 vs “muerto” en mpnet@768 (misma UI) | **Research** (no ticket) | +| [`shared-noise-omit-common.md`](./shared-noise-omit-common.md) | Shared noise: ocultar el pack, dejar el punto distinto (float nativo); SAE capa dudosa | **Draft (hablando — no implementar)** | +| [`open-shared-noise-knob.md`](./open-shared-noise-knob.md) | Parking knob-feel — sucedido por el draft de omit | **Superseded** | +| [`archivo/`](./archivo/) | Epics ya ejecutados o superados | Archivado | + +Fuente de verdad del código: `CHANGELOG.md`, `CONTEXT.md`, `main` @ SemVer en `manifest.json`. + +**UI default (2.4.1+):** una sola app en `/` con chrome Magic Workbench (`src/theme/`). Rutas `/v25/` y `/amiga/` **retiradas**. + +Para una aventura nueva: agregá `roadmap/.md` (+ `PROMPT-…` si hace falta) y una fila en esta tabla. diff --git a/roadmap/archivo/PROMPT-amiga-workbench.md b/roadmap/archivo/PROMPT-amiga-workbench.md new file mode 100644 index 0000000000000000000000000000000000000000..fb6f9314e9d583de377d68170c5c15975009078e --- /dev/null +++ b/roadmap/archivo/PROMPT-amiga-workbench.md @@ -0,0 +1,82 @@ +# Prompt — Amiga Workbench `/amiga` (MagicWB) + +**Modo:** **serial** — una sesión / un slice. No agentes en paralelo. +Empezá por el slice que indique el humano (default: **Slice 1** tras Slice 0 docs). No re-litigar Q1–Q10. + +--- + +```text +Usando dev-protocol, implementá el roadmap Amiga Workbench `/amiga`. + +## Contexto +Repo: VHectorLab 3D / vhectorlab (Python/uv + Vite/Three.js + FastAPI). +Base: `main` actualizado (`git pull`). +SemVer: línea **2.4.x** (Slice 1 cierra con MINOR **2.4.0**). + +Roadmap canónico (leelo COMPLETO antes de código): + `roadmap/amiga-workbench.md` +Lecciones: `.agents/skills/dev-protocol/lessons-learned.md` — **§6.0e obligatorio** + (+ §6.0d v25; §6.0 MPA). + +## AISLAMIENTO (NO negociable) +1. **PROHIBIDO** tocar `src/v25/**`, `v25/**`, o “sync” a v25. +2. **PROHIBIDO** copiar layout/shell de v25 (grid header|left|canvas|right|footer). Amiga = layout **legado** fullscreen + docks flotantes + skin MagicWB. +3. Trabajo nuevo SOLO en `amiga/` + `src/amiga/**`. +4. Legado `/` intacto salvo cambios MPA compartidos mínimos (`vite.mpa.js`, `vite.config.js`, tests MPA). +5. Antes de approval: `git diff -- src/v25 v25` vacío; `src/amiga` sin `data-zone` / zone grid. +6. Este epic **NO** es gui-art / gui-art-v25. + +## Decisiones CERRADAS (no re-preguntar) +- Bandera blanca `/amiga/`; Magic Workbench palette; Topaz self-host; CSS estático. +- Q2-A only (tipo+paleta+chrome mínimo). Cosplay Q2-C = otro roadmap. +- Canvas 3D look no se artea (Q3-A). +- Desktop + mobile; si mobile se complica → preguntar con diseño, no improvisar. +- HF Space = Slice 7 / final solamente. + +## Estilo +No-fluff; TDD donde haya helpers puros (tokens, MPA inputs); módulos profundos. +Seguí `.agents/skills/dev-protocol/SKILL.md` +(clarify → branch `feat/amiga-` → implement → verify → docs → approval gate → git delivery con OK). + +## Slice 1 — DoD (sesión típica de arranque) +1. `amiga/index.html` + `src/amiga/main.js` + `tokens.css` / `style.css`. +2. Topaz self-hosted bajo `src/amiga/assets/fonts/` (+ nota LICENSE en carpeta o README amiga). +3. Clase raíz / `:root` con serrucho (no AA) + font-size 16px (múltiplos de 8) + paleta MagicWB (§3.2 roadmap). +4. Vite MPA: entry `amiga` + redirect `/amiga` → `/amiga/`. +5. Tests MPA actualizados (amiga en `getViteInputs` / redirect). +6. Bump producto a **2.4.0** (manifest / package / CHANGELOG; Navbar legado solo si el DoD de SemVer del repo lo exige — **no** tocar version.js de v25). +7. Smoke: `/` y `/v25/` sin cambios visuales; `/amiga/` se ve MagicWB+Topaz. +8. APPROVAL GATE; ESPERÁ OK antes de push/merge. + +## Slices siguientes +Ver tabla §6 de `roadmap/amiga-workbench.md`. Un slice por sesión. +``` + +--- + +## Prompt — Slice 2+ + +```text +Usando dev-protocol, continuá Amiga `/amiga` — SOLO Slice N. + +Roadmap: `roadmap/amiga-workbench.md` §6 Slice N. +PROHIBIDO: `src/v25/**`, `v25/**`. +Legado `/` solo MPA shared si hace falta. +SemVer: PATCH 2.4.x salvo que el humano diga otro bump. + +DoD: el de la fila Slice N del roadmap. +APPROVAL GATE; no push/merge sin OK. +``` + +--- + +## Prompt — Slice 7 (HF Space) + +```text +Usando dev-protocol, cerrá Amiga `/amiga` — Slice 7 polish + HF Space. + +Roadmap: `roadmap/amiga-workbench.md` §7–§9. +Confirmá con el humano si Space default queda en `/`, `/amiga/`, o link. +PROHIBIDO: tocar v25. +APPROVAL GATE → OK → git delivery + publish Space según proceso del repo. +``` diff --git a/roadmap/archivo/PROMPT-compare-group-contrast-viz.md b/roadmap/archivo/PROMPT-compare-group-contrast-viz.md new file mode 100644 index 0000000000000000000000000000000000000000..115596fde591d4611fc153f16f52b5f43115bfe8 --- /dev/null +++ b/roadmap/archivo/PROMPT-compare-group-contrast-viz.md @@ -0,0 +1,85 @@ +# Prompt — agente COMPARE group contrast visibility + +Copiá y pegá el bloque siguiente en una sesión **nueva**. +Roadmap: [`compare-group-contrast-viz.md`](./compare-group-contrast-viz.md) (2026-08-02). + +**Importante:** hay **decisiones abiertas (D1–D12)**. El agente **debe preguntar y esperar** antes de codear. + +--- + +```text +Usando dev-protocol, ejecutá el roadmap COMPARE group contrast visibility. + +## Contexto +Repo: VectorLab 3D (Python/uv + Vite/Three.js). +Base: `main` actualizado (git pull first). +Roadmap canónico — leelo COMPLETO antes de cualquier código: + `roadmap/compare-group-contrast-viz.md` +Lecciones: `.agents/skills/dev-protocol/lessons-learned.md` + (§2 z-score/tanh, §2.2 viz colors/filter, §4.5 spatial defaults, §4.8 COMPARE groups, + §4.10 Top‑K SAE, §7 SemVer). +Diagnóstico ya corrido (números en roadmap §1): `scripts/diagnose_group_separation.py`. +GROUP_* floating badges bootstrap ya está en main — este epic NO es ese bug; +es legibilidad de contraste G1 vs G2 en la matriz 3D (paint + opcional dim sort). + +## Fase 0 — OBLIGATORIA (antes de branch/código) +1. Leé el roadmap §1 (hechos) y §3 (D1–D12). +2. Preguntá al humano TODAS las decisiones abiertas D1–D12 que aún digan pending + (podés agrupar en un solo mensaje claro). No asumas defaults de producto. +3. ESPERÁ respuestas. Completá roadmap §3.1 (Answers log) con lo acordado. +4. Si algo del diagnóstico §1 contradice lo que el humano ve en UI, pedí screenshot / + RAW vs SAE / Amplitude actual — no inventes. +5. Solo después de D* cerradas: branch e implementación. + +## Decisiones YA CERRADAS (no re-preguntar) +- ANALYSIS: X=dim index, Y=thread stack + val*amplitude, Z=0 (no “tokens on Z”). +- Groups demo sin sort: índices 0–64 GROUP_1, 65–129 GROUP_2. +- SAE train = scope Compare actual (n=130 en el demo); auto_scale ~640 / k=32. +- Global z-score+tanh sobre SAE Top‑K pinta ceros como t≈−0.24 (falso “polvo” negativo). +- Amplitude default COMPARE|ANALYSIS|POINTS = 1.0 hoy → relieve plano (hecho). +- No MESH / no Mode C / no variance slider existentes. +- Carriles: L1 Quick UX → L2 Dim sort → L3 SAE/datos (orden ROI); L4 diagnosis = done. +- Fuera de alcance fijo: PCA/UMAP Projector, Neuronpedia, cambiar contrato /compare, + Arithmetic thread order (salvo que el humano expanda D* explícitamente). + +## Objetivo +Según D1: hacer legible el contraste entre GROUP_* en COMPARE ANALYSIS +(L1 y/o L2 y/o L3). Criterios en roadmap §6 (ajustar tras D*). + +## Flujo post-kickoff +1. Branch: `feat/compare-group-contrast-viz` o stages `…-l1` / `…-l2` si D1 lo pide. +2. Stage A (L1) si aplica: Amplitude default (D2), SAE↔filter (D3/D4), group Y gap (D5). +3. Stage B (L2) si aplica: helper de permutación por contraste + toggle UI (D6–D9). +4. Stage C (L3) solo si D1/D11: datos/normalize (D10) / train guidance. +5. Tests (vitest; re-run `python3 scripts/diagnose_group_separation.py` si L2/L3). +6. CHANGELOG EN + lessons-learned corto (SAE zeros + z-score; Amplitude; dim sort). +7. Smoke roadmap §8. +8. APPROVAL GATE — reportá cómo probar; ESPERÁ OK explícito antes de push/merge + (`git-workflow.md` §3). + +## Criterios de aceptación +Checklist §6 del roadmap + lo que el humano agregue al cerrar D*. + +## Cómo probar (mínimo) +1. Backend + `npm run dev`. +2. COMPARE → 2 Groups → 130 tokens, badges OK. +3. ANALYSIS POINTS: Amplitude default nuevo; con SAE ON verificar filtro (D3) y que + el polvo de ceros normalizados no domine si se eligió + Only. +4. Si L2: toggle dim sort → bandas; OFF → orden crudo. +5. SAE OFF restaura RAW + filtro según D4. + +## Estilo +No-fluff; TDD en helpers puros; módulos profundos; no expandir scope. +Ante duda de contrato UI/SAE: preguntá — no adivines. +Seguí el ciclo completo de `.agents/skills/dev-protocol/SKILL.md` +(clarify → branch → implement → verify → docs → approval gate → git delivery). +``` + +--- + +## Notas para el humano + +1. Pegá el prompt en sesión limpia con `main` al día (incl. fix de badges GROUP si ya mergeó). +2. El agente te va a preguntar **D1–D12** — respondé en bloque; con eso alcanza para codear. +3. Recomendación previa del diagnóstico: **D1 = L1+L2**, **D3 = auto `+ Only` on SAE**, Amplitude **≥16**. Podés confirmar o cambiar. +4. Script de números: `python3 scripts/diagnose_group_separation.py` (backend up). diff --git a/roadmap/archivo/PROMPT-english-ui.md b/roadmap/archivo/PROMPT-english-ui.md new file mode 100644 index 0000000000000000000000000000000000000000..6db6a5db3ccce43d2c0f4ece227c5cece32aba9d --- /dev/null +++ b/roadmap/archivo/PROMPT-english-ui.md @@ -0,0 +1,70 @@ +# Prompt — agente English-only UI (VectorLab 3D) + +Copiá y pegá el bloque siguiente en una sesión nueva. **Glosario y D6–D8 ya están cerrados** en `roadmap/english-ui-i18n.md` (2026-08-02). + +--- + +```text +Usando dev-protocol, ejecutá el roadmap English-only product surface. + +## Contexto +Repo: VectorLab 3D (Python/uv + Vite/Three.js). +Base: `main` actualizado. +Roadmap canónico (leelo completo ANTES de codear): + `roadmap/english-ui-i18n.md` +Lecciones: `.agents/skills/dev-protocol/lessons-learned.md` (§4 paneles, §4.5 sliders, §4.6 labels). + +## Decisiones YA CERRADAS (no re-preguntar) +- D6: Compare auto-vocab → **English** auto-parts lexicon (rename off `_ES`). +- D7: Rename Spanish test titles (`debe…`) to EN; translate ES comments in `src/` + `backend/` to EN. +- D8: **Do NOT rewrite historical CHANGELOG** (leave past entries in Spanish). New/Unreleased entries in EN only. +- Glossary table in roadmap §0.1 is authoritative (ANALYSIS, NAVIGATION, 3D Spatial Controls, Spacing, Vector Distance, Amplitude, Length, Point Thickness, CALCULATE VECTOR, VISUALIZE SEQUENCE, etc.). + +## Objetivo +Product copy + in-scope strings → English. Internal identifiers (`ANALYSIS`, JS keys, CSS) stay unless a visible label requires change. No i18n framework. + +## Fuera de alcance +- `roadmap/archivo/**`, `.agents/**` / skills (except optional short new i18n lesson) +- Rewriting old CHANGELOG sections +- Multi-language / i18n libraries + +## Flujo +### Etapa A — Auditoría exhaustiva +1. Branch `docs/english-ui-audit`. +2. Inventory ES/mixed strings in `src/**`, `index.html`, `tests/**`, `backend/**` (user/API msgs). +3. Deliver `roadmap/english-ui-audit.md` table: `file:line | current | suggested EN | class`. +4. Smoke UI notes. Approval gate → merge docs. + +### Etapa B — SKIP +Already closed in roadmap. If audit finds NEW user-facing terms not in §0.1, list them once and ask; otherwise proceed. + +### Etapa C + D — Apply (can be one branch if clean) +1. Branch `feat/english-user-facing-copy`. +2. Apply glossary to Navbar, Sidebar, ComparePanel, ThreadSliders, LandscapeGate, aria/titles/empty states/placeholders; EN auto vocab. +3. Update UI-copy asserts; rename `debe…` tests to EN; translate ES comments in src/backend. +4. CHANGELOG Unreleased in EN only — leave historical Spanish entries untouched. +5. Vitest green + smoke; approval gate → merge. + +## Criterios de aceptación +- [x] No Spanish visible in UI (Arithmetic, Compare, VIEW, sliders, landscape gate, modals). +- [x] Compare 3D labels remain raw tokens (§4.6). +- [x] Internal `data-view` / mode keys unchanged. +- [x] Tests green; titles EN; comments ES→EN in scope. +- [x] Historical CHANGELOG still Spanish; new notes EN. +- [x] Residual grep of UI Spanish in `src/` clean (or documented false positives). + +## Cómo probar +1. `npm run dev` (+ backend). +2. Walk MODE / VISTA / RENDER; slider titles; Compare empty + sort tooltips; landscape gate; presets show English tokens. +3. Spot-check a few `it('…')` names are English. + +## Estilo +No-fluff; TDD where harness exists; don’t expand scope. +``` + +--- + +## Notas para el humano + +- Glosario ya OK — podés pegar el prompt directo. +- Orden: A (audit) → C+D (apply) con approval gates. diff --git a/roadmap/archivo/PROMPT-etapa-A-collapsible-docks.md b/roadmap/archivo/PROMPT-etapa-A-collapsible-docks.md new file mode 100644 index 0000000000000000000000000000000000000000..3e318ee25de78371ea694beafaa2712191a370cc --- /dev/null +++ b/roadmap/archivo/PROMPT-etapa-A-collapsible-docks.md @@ -0,0 +1,73 @@ +> **ARCHIVO HISTÓRICO** — archivado 2026-08-02. +> **Estado al archivar:** Etapa A entregada. +> **Rama implementada:** `feat/collapsible-side-panels` (merge a `main`, v1.1.0). +> +> Prompt de agente usado para disparar la Etapa A; conservado solo como histórico de sesión. + +# Prompt — siguiente agente (Etapa A del roadmap) + +Copiá y pegá el bloque siguiente en una sesión nueva. + +--- + +```text +Usando dev-protocol, implementá SOLO la Etapa A del roadmap de paneles colapsables / mobile / mesh / ribbons. + +## Contexto +Repo: VectorLab 3D (Python/uv backend + Vite/Three.js frontend). +Base branch: `main` (actualizado). +Roadmap canónico (leelo completo antes de codear): + `roadmap/mobile-panels-mesh-ribbons.md` + +Lecciones / invariantes (OBLIGATORIO leer): + `.agents/skills/dev-protocol/lessons-learned.md` + En especial §1 WebGL, §3 navegación, §4.1 / §4.2 paneles (fit-content, overflow hidden, scroll solo en listas internas; COMPARE lista↔3D sync). + +## Alcance de ESTA sesión = Etapa A únicamente +NO implementes aún: responsive mobile (B), touch nav (C), MESH (D), RIBBONS (E). +Sí: docks colapsables izquierda/derecha con pestaña. + +### Decisiones de producto ya cerradas (D1–D4, D10) +- Dock IZQUIERDA: host de Arithmetic O Compare según MODE. Una pestaña/estado de minimize para el dock izq. activo. +- Dock DERECHA: sliders 3D (`#thread-sliders-container`) + gizmo/HUD de ese lado (`AxisGizmo` y chrome derecho asociado). +- HUD inferior de telemetría (`HUD.js` barra bottom): SIEMPRE VISIBLE — no colapsa. +- Desktop: recordar collapsed en `localStorage`. +- Mobile default collapsed es Etapa B — en A podés dejar desktop defaults abiertos o preparar el hook; no hace falta el overlay landscape todavía. +- Ship por etapa: rama `feat/collapsible-side-panels`, approval gate antes de merge. + +### Comportamiento Etapa A +- Izquierda colapsa hacia `left`; queda pestaña vertical clickeable (▶ / handle). +- Derecha colapsa hacia `right`; pestaña (◀). +- Transición CSS suave ~250ms (`transform`); NO desmontar el DOM (preservar form, sliders, lista cosine). +- `pointer-events: none` en el panel off-screen excepto la pestaña. +- Al cambiar MODE Arithmetic↔Compare, el dock izq. activo respeta la misma política de collapsed (documentá la regla elegida). +- NO reintroducir scrollbar externa en `.glass-sidebar` / compare (§4.1 / §4.2). + +### Archivos probables +`src/ui/Sidebar.js`, `src/ui/ComparePanel.js`, `src/ui/ThreadSliders.js`, `src/visualizer/AxisGizmo.js`, `src/ui/HUD.js`, `src/main.js`, `src/style.css` +Preferí un helper/módulo deep pequeño tipo `CollapsibleDock` si reduce duplicación. + +### Criterios de aceptación (A) +- [ ] Click pestaña izq. oculta Arithmetic/Compare activo; click restaura. +- [ ] Click pestaña der. oculta sliders (+ gizmo asociado); click restaura. +- [ ] HUD inferior sigue visible en todo momento. +- [ ] Cambio MODE no rompe el estado collapsed del dock izq. +- [ ] Sin scrollbar externa del panel. +- [ ] Tests vitest mínimos del toggle / `aria-expanded` si el harness lo permite. +- [ ] CHANGELOG corto + lesson en lessons-learned si descubrís invariante nueva de docks. + +### Flujo +1. Branch `feat/collapsible-side-panels` desde `main`. +2. TDD / vertical slice; verde local (`vitest`, smoke UI). +3. Approval gate: reportá cómo probar y ESPERÁ OK antes de push/merge a main. + +### Fuera de alcance ahora +Portrait overlay, joystick mobile, MESH surface, wide RIBBONS, cambios de colormap, API salvo que A lo exija (no debería). +``` + +--- + +## Notas para vos (humano) + +- Después de mergear A, el próximo prompt es Etapa B (mismo archivo de roadmap, sección B + D13 overlay suave). +- Capturas de referencia MESH/RIBBONS quedaron en la sesión Cursor; si el próximo agente hace D/E, adjuntás esas PNGs o las copiás a `roadmap/refs/`. diff --git a/roadmap/archivo/PROMPT-galaxy-feel-group-hue.md b/roadmap/archivo/PROMPT-galaxy-feel-group-hue.md new file mode 100644 index 0000000000000000000000000000000000000000..7a7bfe68d4069a78c7c573a5693225d358196ca3 --- /dev/null +++ b/roadmap/archivo/PROMPT-galaxy-feel-group-hue.md @@ -0,0 +1,123 @@ +# Prompt — agente Galaxy feel + Group hue + +Copiá y pegá el bloque siguiente en una sesión **nueva**. +Roadmap: [`galaxy-feel-group-hue.md`](./galaxy-feel-group-hue.md) (2026-08-08). + +**Modo de trabajo:** **serial** — una sesión / un slice. No lanzar agentes en paralelo. +Empezá por **Slice 1** salvo que el humano diga otro. + +--- + +```text +Usando dev-protocol, ejecutá el roadmap Galaxy feel + Group hue. + +## Contexto +Repo: VHectorLab 3D / vhectorlab (Python/uv + Vite/Three.js). +Base: `main` actualizado (git pull first si aplica). +Roadmap canónico — leelo COMPLETO antes de cualquier código: + `roadmap/galaxy-feel-group-hue.md` +Lecciones: `.agents/skills/dev-protocol/lessons-learned.md` + — §3 WASD lerp; §4.11b group paint; §6.0d PROHIBIDO `src/v25/**`. +UI legacy only: `src/main.js`, `src/ui/*`, `src/visualizer/*`, `src/engine/*` +PROHIBIDO: tocar cualquier archivo bajo `src/v25/`. + +## Decisiones YA CERRADAS (no re-preguntar) +- Galaxy: agrandar mundo UMAP + bajar WASD/QE + bajar mouse look — solo en GALAXY; al salir restaurar. +- Calibración elegante a juicio del agente (constantes documentadas); sin knobs nuevos de speed en v1. +- “Poca definición” = muy juntos + cuadrados → más escala + soft stars (disco circular con glow), no InstancedMesh spheres. +- Stars solo Galaxy POINTS; ANALYSIS/NAVIGATION intactos salvo helper compartido opt-in. +- Group hue bajo Group contrast, default OFF, ≥2 GROUP_*. +- Ramp: black (−1) → color picker del grupo (+1); transversal COMPARE (ANALYSIS/NAV/GALAXY; POINTS+RIBBONS donde ya pinta viz). +- Color picker+hex por GROUP_* (dinámico); persist `groupHueEnabled` + `groupHueColors`. +- Convive: hue reemplaza base divergent; Shared noise / Sign conflict siguen encima. +- t = activation normalizada existente; Galaxy sigue con cosine_vs_first como activation. +- Tips EN estilo fieldInfo; tip Group hue puede ser un poco más largo para ser explícito. +- SemVer: PATCH en línea 2.3.x salvo que humano diga otro. + +## Slice a ejecutar en ESTA sesión +Slice 1 — Galaxy world scale + flight profile. + +Si el humano indica otro slice del roadmap §6, hacé SOLO ese. +No adelantar slices siguientes “por si acá”. + +### Definition of Done — Slice 1 +1. Mundo Galaxy más grande (retocar `GALAXY_DEFAULT_SCALE` y/o mapping `threadSpacing` en `renderGalaxyData`) en sintonía con vuelo más lento. +2. Flight profile Galaxy-only en `Navigation` (o helper): moveSpeed + mouse look más bajos; leave Galaxy restaura defaults actuales. +3. Camera framing IT-core / bbox sigue OK tras el scale. +4. Tests unitarios del apply/restore de profile (y scale helper si aplica). +5. `npm test` verde en lo tocado; `git diff -- src/v25` vacío. +6. Branch `feat/galaxy-feel-group-hue` (o stage `feat/galaxy-flight-scale`). +7. APPROVAL GATE: cómo probar WASD/QE/mouse en Galaxy vs ANALYSIS; ESPERÁ OK antes de push/merge. + +### Slices siguientes (NO implementar ahora; solo contexto) +2 Soft star POINTS (Galaxy) +3 Group hue UI + paint transversal +4 Polish/docs/CHANGELOG/lessons/smoke + +## Estilo +No-fluff; TDD; módulos profundos; no expandir scope. +Ante duda de contrato: preguntá — no adivines. +Seguí `.agents/skills/dev-protocol/SKILL.md` +(clarify → branch → implement → verify → docs → approval gate → git delivery con OK). +``` + +--- + +## Prompt — Slice 2 (pegar cuando Slice 1 esté OK/mergeado) + +```text +Usando dev-protocol, continuá Galaxy feel + Group hue — SOLO Slice 2. + +Roadmap: `roadmap/galaxy-feel-group-hue.md` §3.3 + §6 Slice 2. +PROHIBIDO: `src/v25/**`. + +DoD: +- Galaxy POINTS: soft circular “stars” con glow hacia el color final (fragment mask radial; no squares). +- Tamaño / clamp `gl_PointSize` en sintonía con el scale de Slice 1. +- Non-Galaxy POINTS sin regresión visual. +- Tests o flag/helper verificable; APPROVAL GATE; no push/merge sin OK. +``` + +--- + +## Prompt — Slice 3 + +```text +Usando dev-protocol, continuá Galaxy feel + Group hue — SOLO Slice 3. + +Roadmap: `roadmap/galaxy-feel-group-hue.md` §3.4–§3.5 + §4–§5 + §6 Slice 3. +PROHIBIDO: `src/v25/**`. + +DoD: +- Toggle Group hue (default OFF) bajo Group contrast + “i” explícita EN. +- Color picker+hex dinámico por GROUP_*; persistencia; gate ≥2 grupos. +- Paint: black (−1) → group color (+1); base reemplaza divergent cuando ON; Shared noise / Sign conflict siguen. +- Transversal COMPARE (ANALYSIS/NAV/GALAXY; POINTS+RIBBONS donde aplica viz paint). +- Tests paint + persist + gate; APPROVAL GATE. +``` + +--- + +## Prompt — Slice 4 (cierre) + +```text +Usando dev-protocol, cerrá Galaxy feel + Group hue — SOLO Slice 4 (polish/docs). + +Roadmap: `roadmap/galaxy-feel-group-hue.md` §7–§10. +PROHIBIDO: `src/v25/**` (verificar `git diff -- src/v25` vacío). + +DoD: +- CHANGELOG EN; CONTEXT si hace falta; lessons-learned (flight profile constants, star disc, group hue layering + zero-coverage choice). +- Smoke §7 en el reporte. +- SemVer PATCH confirmado con humano si hace falta. +- APPROVAL GATE → OK → git delivery según protocol. +``` + +--- + +## Notas para el humano + +1. Un prompt de slice por sesión. Empezá por Slice 1. +2. Después de cada APPROVAL GATE, dale OK explícito antes de push/merge. +3. Si el agente propone constantes de scale/speed, pedile el valor final en el reporte y probá 30s en Galaxy antes de OK. +4. Group hue es transversal: validá también ANALYSIS, no solo Galaxy. diff --git a/roadmap/archivo/PROMPT-galaxy-view-continue.md b/roadmap/archivo/PROMPT-galaxy-view-continue.md new file mode 100644 index 0000000000000000000000000000000000000000..947aadd372b1627dccbe91fc02f6bbd332306857 --- /dev/null +++ b/roadmap/archivo/PROMPT-galaxy-view-continue.md @@ -0,0 +1,111 @@ +# Prompt — continuar Galaxy VIEW (post Slice 4 / v2.3.0) + +**Modo:** **serial** — una sesión / un slice. No agentes en paralelo. +Empezá por **Slice 5** (hay WIP parcial en rama). No re-litigar decisiones cerradas. + +--- + +```text +Usando dev-protocol, continuá el roadmap Galaxy VIEW desde donde quedó. + +## Estado YA en main (NO rehacer) +Repo: VHectorLab 3D / vhectorlab (Python/uv + Vite/Three.js). +Versión producto: **2.3.0** (línea **2.3.x** de acá en más — PATCH para el resto del epic salvo que el humano diga otro MINOR). +Base: `main` actualizado (`git pull`). + +Hecho y mergeado: +- Slice 1: `POST /project` UMAP (`backend/projection.py`, `umap-learn==0.5.12`, tests, `architecture_spec.md`) +- Slice 2: VIEW GALAXY + chips UMAP|PCA|t-SNE (gris) + locks COMPARE+POINTS (`src/ui/galaxyChrome.js`, Navbar, main) +- Slice 3: bootstrap `GROUP_it_core` (100) + GROUP_1 + GROUP_2 (`src/ui/itCoreCorpus.js`, ComparePanel) +- Slice 4: layout 1 punto/token, badges centroides, camera IT core, sin ribbons (`src/visualizer/galaxyLayout.js`, `Instancer.renderGalaxyData`, `RemoteProvider.projectEmbeddings`, `main.ensureGalaxyProjection`) + +Roadmap canónico (leelo COMPLETO antes de código): + `roadmap/galaxy-view.md` +Lecciones: `.agents/skills/dev-protocol/lessons-learned.md` — **§6.0d obligatorio**: + **PROHIBIDO tocar `src/v25/**` salvo que el humano lo diga explícitamente.** + Antes de merge: `git diff -- src/v25` vacío. +UI legacy only: `src/main.js`, `src/ui/*`, `src/visualizer/*` + +## Decisiones CERRADAS (no re-preguntar) +- VIEW GALAXY; MODE fijo COMPARE; RENDER fijo POINTS. +- DR 100% backend `/project`; default UMAP; PCA/t-SNE 501 / chips gray. +- Cósmico 3D (n_components=3). 2D = epic futuro. +- SAE aplica (vectores SAE → /project). +- Progress UI obligatorio: status + barra + paso k/n. +- Orquestación v1: client-driven steps. +- Token Comparison panel: comportamiento actual (cosine, reorder, sort rules). +- SemVer: seguir en **2.3.x** (PATCH) para Slices 5–7 salvo indicación contraria. No bump 2.4 por cada slice. + +## WIP local (rama / working tree) +Puede existir rama `feat/galaxy-pipeline-progress` con trabajo a medias de Slice 5: +- `src/ui/galaxyPipeline.js` + `tests/galaxyPipeline.test.js` (helpers steps/fingerprint/cache) +- `ComparePanel`: markup `#galaxy-progress` + `setGalaxyProgress` / `clearGalaxyProgress` +- `CHANGELOG.md` reordenado (Unreleased vs [2.3.0]) +- `lessons-learned.md` §6.0d (v25 off-limits) + +`main.js` todavía usa `ensureGalaxyProjection` (sin progress k/n ni pipeline SAE completo). + +Acción al arrancar: +1. `git status` / checkout o recreá `feat/galaxy-pipeline-progress` desde `main`. +2. Si el WIP es útil, reusalo; si está podrido, descartá y reimplementá limpio sobre `main`. +3. Completá **SOLO Slice 5** en esta sesión. + +## Slice 5 — DoD (ESTA sesión) +Roadmap §2.2 progress + §6 Slice 5. + +1. Pipeline client-driven: encode → SAE? → UMAP `/project` → build galaxy. +2. UI: status text + progress bar + paso **k/n** siempre visible mientras trabaja. +3. Visualize y toggle SAE (en Galaxy) disparan pipeline con feedback. +4. Reusar cache `/compare` si texts no cambiaron (`compareTextsFingerprint` / `canReuseCompareCache` si ya existen). +5. Tests verdes (`npm test` + lo que toques). +6. Sin `src/v25/**`. +7. APPROVAL GATE: cómo probar (smoke roadmap §8 pasos 3–5); ESPERÁ OK antes de push/merge. +8. Tras OK: commit → push → merge a `main` (protocol git-workflow). Versión: PATCH **2.3.1** si el humano quiere tag ahora, o dejar 2.3.0 y documentar en CHANGELOG Unreleased/2.3.0 — **preguntá solo si hay duda**; default razonable: append a sección `[2.3.0]` o Unreleased sin bump hasta Slice 7, salvo que el humano diga bump. + +### Slices siguientes (NO implementar ahora; solo contexto) +6 — Botón K-means disabled + info tip EN (no clustering). +7 — Polish/docs: CONTEXT.md Galaxy/UMAP; CHANGELOG; lessons (layout≠threads, progress, umap seed); smoke §8 completo; verificar `git diff -- src/v25` vacío. + +## Estilo +No-fluff; TDD; módulos profundos; no expandir scope. +Ante duda de contrato: preguntá. +Seguí `.agents/skills/dev-protocol/SKILL.md` +(clarify → branch → implement → verify → docs → approval gate → git delivery con OK). +``` + +--- + +## Prompt — Slice 6 (sesión siguiente, tras Slice 5 OK) + +```text +Usando dev-protocol, continuá Galaxy VIEW — SOLO Slice 6. + +Roadmap: `roadmap/galaxy-view.md` §2.3 + §6 Slice 6. +PROHIBIDO: `src/v25/**` (§6.0d lessons-learned). +Versión: línea 2.3.x. + +DoD: +- Botón K-means (disabled / coming soon) + info tip claro en inglés de producto. +- No implementar clustering. +- APPROVAL GATE; no push/merge sin OK. +``` + +--- + +## Prompt — Slice 7 (cierre) + +```text +Usando dev-protocol, cerrá Galaxy VIEW — SOLO Slice 7 (polish/docs). + +Roadmap: `roadmap/galaxy-view.md` §7–§10. +PROHIBIDO: `src/v25/**` (verificar `git diff -- src/v25` vacío). +Versión: confirmar con humano si cerrar como 2.3.0 completo o bump PATCH 2.3.x. + +DoD: +- CONTEXT.md: Galaxy VIEW, UMAP, galactic core / GROUP_it_core. +- CHANGELOG EN actualizado. +- lessons-learned: layout ≠ dim threads; progress k/n; umap seed; §6.0d ya existe. +- architecture_spec `/project` completo si faltó algo. +- Smoke §8 completo en el reporte. +- APPROVAL GATE → OK → git delivery. +``` diff --git a/roadmap/archivo/PROMPT-galaxy-view.md b/roadmap/archivo/PROMPT-galaxy-view.md new file mode 100644 index 0000000000000000000000000000000000000000..60c05cebefa22a85f86cf93b5bce75c6662b1591 --- /dev/null +++ b/roadmap/archivo/PROMPT-galaxy-view.md @@ -0,0 +1,184 @@ +# Prompt — agente Galaxy VIEW (UMAP) + +Copiá y pegá el bloque siguiente en una sesión **nueva**. +Roadmap: [`galaxy-view.md`](./galaxy-view.md) (2026-08-08). + +**Modo de trabajo:** **serial** — una sesión / un slice. No lanzar agentes en paralelo. +Empezá por **Slice 1** salvo que el humano diga otro. + +--- + +```text +Usando dev-protocol, ejecutá el roadmap Galaxy VIEW (UMAP word universe). + +## Contexto +Repo: VectorLab 3D / VHectorLab (Python/uv + Vite/Three.js). +Base: `main` actualizado (git pull first si aplica). +Roadmap canónico — leelo COMPLETO antes de cualquier código: + `roadmap/galaxy-view.md` +Lecciones: `.agents/skills/dev-protocol/lessons-learned.md` +Arquitectura API: `architecture_spec.md` +UI legacy only: `src/main.js`, `src/ui/*`, `src/visualizer/*` +PROHIBIDO: tocar cualquier archivo bajo `src/v25/` (proyecto aparte). + +## Decisiones YA CERRADAS (no re-preguntar) +- VIEW nuevo: GALAXY (junto a ANALYSIS | NAVIGATION). +- En Galaxy: MODE fijo COMPARE, RENDER fijo POINTS; tabs MODE/RENDER locked. +- Cósmico 3D ahora (UMAP n_components=3). 2D VIEW = epic futuro (documentado). +- DR 100% backend: POST /project; default method=umap. +- Bajo el control GALAXY: chips chicos UMAP | PCA | t-SNE; PCA y t-SNE grisados. +- Bootstrap: GROUP_it_core (100 IT EN del roadmap §4) + GROUP_1 + GROUP_2 demo actual. +- Clusters visuales = GROUP_*; badges + hover labels. +- Botón K-means + info tip claros ahora; implementación después. +- SAE aplica (embeddings transformados → /project). +- Progress UI obligatorio: status + barra + paso k/n. +- Token Comparison panel: mismo comportamiento (cosine, reorder, sort rules). +- Orquestación v1: client-driven steps (compare → SAE? → project → build). + +## Slice a ejecutar en ESTA sesión +Slice 1 — Backend POST /project (UMAP only). + +Si el humano indica otro slice del roadmap §6, hacé SOLO ese. +No adelantar slices siguientes “por si acá”. + +### Definition of Done — Slice 1 +1. `uv add umap-learn` (y lo que uv resuelva); sync OK. +2. Endpoint `POST /project` según roadmap §3.1 (umap + n_components 2|3; seed; params). +3. method pca/tsne → error claro (400 o 501); umap sole green path. +4. Tests `uv run pytest` para validación + smoke proyección (n chico, seed fijo). +5. Nota en `architecture_spec.md` del contrato. +6. Branch `feat/galaxy-view` (o `feat/galaxy-project-api` si preferís stage branch). +7. APPROVAL GATE: reportá cómo probar con curl/httpie; ESPERÁ OK antes de push/merge. + +### Slices siguientes (NO implementar ahora; solo contexto) +2 Navbar Galaxy + locks + chips +3 Bootstrap corpus IT+G1+G2 +4 Layout/Instancer 1 punto/token +5 Pipeline + progress k/n +6 K-means stub +7 Polish/docs + +## Estilo +No-fluff; TDD; módulos profundos; no expandir scope. +Ante duda de contrato: preguntá — no adivines. +Seguí `.agents/skills/dev-protocol/SKILL.md` +(clarify → branch → implement → verify → docs → approval gate → git delivery con OK). +``` + +--- + +## Prompt — Slice 2 (pegar en la sesión siguiente, cuando Slice 1 esté mergeado/OK) + +```text +Usando dev-protocol, continuá Galaxy VIEW — SOLO Slice 2. + +Roadmap: `roadmap/galaxy-view.md` §6 Slice 2 + §2.1 chrome. +PROHIBIDO: `src/v25/**`. + +DoD: +- Tab VIEW GALAXY en Navbar legacy. +- Sub-chips UMAP (activo) | PCA | t-SNE (grisados + title/info “coming next”). +- Entrar a Galaxy fuerza COMPARE + POINTS y deshabilita MODE/RENDER. +- Salir de Galaxy restaura triad previa / re-habilita tabs. +- Tests UI/chrome donde el repo ya testeé Navbar/defaults. +- APPROVAL GATE; no push/merge sin OK. +``` + +--- + +## Prompt — Slice 3 + +```text +Usando dev-protocol, continuá Galaxy VIEW — SOLO Slice 3. + +Roadmap: `roadmap/galaxy-view.md` §4 corpus + §6 Slice 3. +PROHIBIDO: `src/v25/**`. + +DoD: +- Bootstrap Compare = GROUP_it_core (exactamente 100 tokens del §4) + GROUP_1 + GROUP_2 demos existentes. +- getCompareBootstrap / presets actualizados; tests de parse/count/group ids. +- ORDER: it_core primero (REF dentro del núcleo salvo edición user). +- APPROVAL GATE. +``` + +--- + +## Prompt — Slice 4 + +```text +Usando dev-protocol, continuá Galaxy VIEW — SOLO Slice 4. + +Roadmap: `roadmap/galaxy-view.md` §5.2 layout + §6 Slice 4. +Depende de /project (Slice 1) disponible. +PROHIBIDO: `src/v25/**`. + +DoD: +- Layout Galaxy: 1 punto por token desde positions[3]. +- Group badges en centroides; hover token labels. +- Camera framed al centroide de GROUP_it_core (fallback bbox all). +- Sin ribbons dim-axis en Galaxy. +- Tests de helpers de layout. +- APPROVAL GATE. +``` + +--- + +## Prompt — Slice 5 + +```text +Usando dev-protocol, continuá Galaxy VIEW — SOLO Slice 5. + +Roadmap: `roadmap/galaxy-view.md` §2.2 progress + §6 Slice 5. +PROHIBIDO: `src/v25/**`. + +DoD: +- Pipeline client-driven: encode → SAE? → UMAP /project → build galaxy. +- UI: status text + progress bar + paso k/n siempre visible durante el trabajo. +- Visualize y toggle SAE disparan pipeline con feedback. +- Reusar cache /compare si texts no cambiaron (si ya existe patrón). +- APPROVAL GATE + smoke roadmap §8 pasos 3–5. +``` + +--- + +## Prompt — Slice 6 + +```text +Usando dev-protocol, continuá Galaxy VIEW — SOLO Slice 6. + +Roadmap: `roadmap/galaxy-view.md` §2.3 + §6 Slice 6. +PROHIBIDO: `src/v25/**`. + +DoD: +- Botón K-means (disabled / coming soon) + info tip claro en inglés de producto. +- No implementar clustering. +- APPROVAL GATE. +``` + +--- + +## Prompt — Slice 7 + +```text +Usando dev-protocol, cerrá Galaxy VIEW — SOLO Slice 7 (polish/docs). + +Roadmap: `roadmap/galaxy-view.md` §7–§10. +PROHIBIDO: `src/v25/**` (verificar `git diff -- src/v25` vacío). + +DoD: +- CONTEXT.md términos Galaxy/UMAP/galactic core. +- CHANGELOG EN (MINOR si human confirma). +- lessons-learned (layout ≠ threads; progress; umap seed). +- architecture_spec ya tiene /project; completar si faltó. +- Smoke §8 completo en el reporte. +- APPROVAL GATE → esperar OK → git delivery según protocol. +``` + +--- + +## Notas para el humano + +1. Trabajá **un prompt de slice por sesión**. Empezá por Slice 1. +2. Después de cada APPROVAL GATE, dale OK explícito antes de push/merge. +3. Epics futuros (PCA, t-SNE, 2D VIEW, K-means real) no van en estos prompts. +4. Si un slice necesita decisión menor (¿400 vs 501 para pca?, ¿dónde vive la barra de progreso?), el agente debe preguntar — son detalles no bloqueantes del plan. diff --git a/roadmap/archivo/PROMPT-gui-art-v25.md b/roadmap/archivo/PROMPT-gui-art-v25.md new file mode 100644 index 0000000000000000000000000000000000000000..476a5009cb0263f178bf37a8ccb86e51acf7ae32 --- /dev/null +++ b/roadmap/archivo/PROMPT-gui-art-v25.md @@ -0,0 +1,66 @@ +# Prompt — agente VHectorLab-3D `/v25/` (una fase por sesión) + +Copiá el bloque siguiente en una sesión **nueva**. Completá `FASE_N` y el nombre de fase. + +**Fuente de verdad:** [`gui-art-v25.md`](./gui-art-v25.md) +**Protocolo:** `.agents/skills/dev-protocol/SKILL.md` + módulos bajo demanda + `lessons-learned.md` **siempre**. + +--- + +```text +Usando dev-protocol, ejecutá SOLO la Fase FASE_N del roadmap `roadmap/gui-art-v25.md` (VHectorLab-3D /v25/). + +## Reglas duras +- Marca siempre: **VHectorLab-3D** (nunca “Quantum Vector Lab”). +- Trabajo nuevo SOLO bajo `/v25/` (`v25/index.html`, `src/v25/**`). No romper ni “arreglar” el legado en `/` salvo smoke check. +- Una fase por sesión. No adelantar la siguiente. +- Stack: vanilla JS + Vite MPA. No React/TS/Tailwind. +- Backend/API: no cambiar contratos `/api`. +- HF Space: NO publicar. +- Approval gate: al terminar, reportá + esperá OK humano antes de push/merge. +- Al cerrar: actualizar tabla §10 + hand-off (plantilla §5) en `roadmap/gui-art-v25.md` y lessons si hay invariante nueva (`v25:`). + +## Antes de codear +1. `git pull` en `main`; branch `feat/v25-` según la fase. +2. Leé: `roadmap/gui-art-v25.md` (tu fase + §1–§3 + hand-off de la fase anterior). +3. Leé lessons-learned (WebGL/nav/UI/mobile/EN/tips/SemVer según aplique). +4. Si el hand-off anterior marca riesgos abiertos, resolvé o preguntá — no asumas. + +## Norte visual (fases chrome) +- Dark/fluo lab, no neón chillón; botones 3D pressable; chapa (menos glass). +- Sin portal de fondo. +- Copy EN técnico corto; reducir emoji. +- Canvas es héroe solo cuando la fase lo incluya. + +## DoD +El DoD de tu fase en `gui-art-v25.md` §6–§7. Auto-verificá: tests + `/v25/` + smoke `/`. +Hand-off explícito para el próximo agente. +``` + +--- + +## Cómo usar (operador humano) + +1. Abrí sesión nueva. +2. Pegá el prompt con p.ej. `FASE_N = 1` y slug mental `scaffold`. +3. Probá el DoD. +4. OK explícito → el agente entrega git (sin HF). +5. Sesión nueva para Fase 2, etc. + +## Mapa rápido fase → slug sugerido + +| Fase | Slug branch | +| :---: | :--- | +| 1 | `feat/v25-scaffold` | +| 2 | `feat/v25-tokens` | +| 3 | `feat/v25-shell` | +| 4 | `feat/v25-header` | +| 5 | `feat/v25-arithmetic-chrome` | +| 6 | `feat/v25-right-hud-chrome` | +| 7 | `feat/v25-arithmetic-wire` | +| 8 | `feat/v25-canvas-wire` | +| 9 | `feat/v25-spatial-wire` | +| 10 | `feat/v25-compare` | +| 11 | `feat/v25-sae-viz` | +| 12 | `feat/v25-mobile` | +| 13 | `feat/v25-polish` | diff --git a/roadmap/archivo/PROMPT-gui-art.md b/roadmap/archivo/PROMPT-gui-art.md new file mode 100644 index 0000000000000000000000000000000000000000..2a67191aa4b79466024534c84906fce7ccbcb689 --- /dev/null +++ b/roadmap/archivo/PROMPT-gui-art.md @@ -0,0 +1,48 @@ +# Prompt — agente GUI & Art (VHectorLab 3D) + +Copiá y pegá el bloque siguiente en una sesión nueva. + +--- + +```text +Usando dev-protocol, trabajá el epic GUI & Art de VHectorLab 3D. + +## Contexto +Repo: `vhectorlab` (Python/uv + Vite/Three.js + FastAPI). +Base: `main` actualizado (`git pull` primero). +Producto en HF: `hbauzan/llm-semantic-visualizer` (sdk docker, cpu-basic). +Versión actual de referencia: ver `manifest.json` / Navbar `version-tag`. + +Leé ANTES de codear: +1. `.agents/skills/dev-protocol/SKILL.md` + `lessons-learned.md` (sobre todo §1 WebGL, §3 nav, §4 UI, §4.1/4.1b mobile, §4.7 EN, §4.13 tips, §7 SemVer) +2. `roadmap/gui-art.md` (norte + inventario + decisiones abiertas A1–A6) +3. `CONTEXT.md` (glosario) +4. `src/style.css` `:root` tokens + glass + mobile MQ +5. `src/ui/appViewDefaults.js` (startup ARITHMETIC|ANALYSIS|POINTS) + +## Norte (no negociar sin preguntar) +- El canvas 3D es el héroe; chrome = cristal fino, no dashboard genérico. +- Portrait-first en phone; landscape corto sigue mobile chrome (`MOBILE_MQ`). +- Copy EN only. Tips “i” cortos. Sin landscape-gate. +- No romper scroll Top-10 / cosine ni docks colapsables. + +## Primero: preguntame A1–A6 de roadmap/gui-art.md +No asumas tipografía, emoji, rampa de color, ni scope del primer slice. +Cerrá las respuestas en `roadmap/gui-art.md` y recién ahí codeá. + +## Estilo de trabajo +- Branch `feat/gui-art-` desde `main`. +- Vertical slices + TDD en helpers puros. +- Auto-verificar: desktop + phone portrait + phone landscape (DevTools o real). +- Sync docs condicional (CHANGELOG / lessons / CONTEXT si cambia lenguaje de dominio). +- SemVer: PATCH para polish; MINOR si aparece superficie nueva de producto. +- Hand off en approval gate. Con OK explícito: commit → push → merge main → push main → publicar HF Space (`git push --force` a `https://huggingface.co/spaces/hbauzan/llm-semantic-visualizer` HEAD:main, HF_SPACE_FORCE_PUSH=1). + +## Fuera de alcance +Backend/SAE math, MESH, landscape-gate, i18n framework, refactors masivos sin slice visual demoable. + +## Definition of done (slice) +- Se ve claramente mejor en al menos una superficie (chrome 2D o look 3D). +- Tests verdes; mobile no regresa a “lista cortada” / sin joystick en landscape. +- Lecciones actualizadas si descubrís una invariante visual nueva. +``` diff --git a/roadmap/archivo/PROMPT-sae-denoise.md b/roadmap/archivo/PROMPT-sae-denoise.md new file mode 100644 index 0000000000000000000000000000000000000000..623a072bbf1a4779325a459e86b51e7ab5bc35c8 --- /dev/null +++ b/roadmap/archivo/PROMPT-sae-denoise.md @@ -0,0 +1,62 @@ +# Prompt — agente Top‑K SAE Clean/Denoise (trained) + +Copiá y pegá el bloque siguiente en una sesión nueva. **Decisiones cerradas** en `roadmap/sae-denoise.md` (2026-08-02). +**Importante:** esto es un **Top‑K SAE real (PyTorch, entrenado)**, NO la proyección sinusoidal descartada. + +--- + +```text +Usando dev-protocol, ejecutá el roadmap Top‑K SAE Clean/Denoise. + +## Contexto +Repo: VectorLab 3D (Python/uv + Vite/Three.js). +Base: `main` actualizado (pull first). +Roadmap canónico (leelo COMPLETO antes de codear): + `roadmap/sae-denoise.md` +Lecciones: `.agents/skills/dev-protocol/lessons-learned.md` + (§4 docks/UI, §6.1 /api proxy, §7 SemVer MAJOR 2.0.0). + +## Decisiones YA CERRADAS (no re-preguntar) +- U1–U5: CTA 50/50 `Clean/Denoise (SAE)` toggle; localStorage; params/train UI; progress. +- M1–M2: Replace Compare vectors when SAE ON; cache raw 768. **No SAE in Arithmetic.** +- A1–A7: Real Top‑K SAE (grandmother port): 768→8192, k=32, ReLU+TopK FP32, untied unit-norm decoder, train MSE; viz uses encode activations. +- B1–B6: Backend PyTorch; ephemeral session model; /api/sae/status|train|encode|clear; train on **current scope embeddings** (not vocab); no product checkpoint. +- F1: raw_i vs sae_i key isolation if needed. +- X1: No Gemini labels; no sinusoidal fake SAE; no dual mesh. +- V1: Ship **2.0.0**. + +## Objetivo +Port TopKSAE + SAEManager + train_sae; wire API; UI toggle/train/progress; replace render pipeline; docs + v2.0.0. + +## Fuera de alcance +Sinusoidal “SAE-style”, Gemini feature naming, dual RAW+SAE 3D, PDF relief-matrix from grandmother, full-vocab train. +## Flujo +1. Branch `feat/topk-sae-denoise`. +2. Stage A: model+train+pytest. +3. Stage B: FastAPI routes + progress. +4. Stage C: UI 50/50 + encode replace + metrics. +5. Docs/CHANGELOG/CONTEXT/lessons; bump 2.0.0. +6. pytest + vitest green; smoke §6–§7. +7. APPROVAL GATE — wait for explicit OK before push/merge. + +## Criterios de aceptación +Checklist §7 del roadmap. + +## Cómo probar +1. Backend + frontend. +2. Train SAE on current Visualize/Calculate scope (progress epochs; dims may auto-scale). +3. Compare Visualize → Train SAE → toggle SAE → threads length = resolved hidden_dim sparse; OFF → 768. +4. Arithmetic stays RAW-only (no Clean/Denoise UI). +5. New Visualize clears session SAE; must re-train. Reload starts untrained (ephemeral). + +## Estilo +No-fluff; deep modules; port grandmother numerics faithfully; don’t re-ask closed IDs. +``` + +--- + +## Notas para el humano + +- La abuela ya tenía el SAE **serio** (Top‑K entrenado). El paste sinusoidal de otra herramienta era un atajo distinto — **descartado**. +- Entrenamiento = embeddings del **scope actual** (Compare/Arithmetic), modelo efímero en RAM; dims auto-scale si N es chico. +- Pegá este prompt en sesión limpia tras mergear lo pendiente a `main` si hace falta. diff --git a/roadmap/archivo/PROMPT-shared-noise-common-mode.md b/roadmap/archivo/PROMPT-shared-noise-common-mode.md new file mode 100644 index 0000000000000000000000000000000000000000..192e8655700792ec9dd84b8b39dc69bbe12dd61c --- /dev/null +++ b/roadmap/archivo/PROMPT-shared-noise-common-mode.md @@ -0,0 +1,85 @@ +# Prompt — agente Shared noise common-mode + +> **Archivado:** 2026-08-26 — no usar. Sucesor (también archivado, shipped 2.4.3): [`PROMPT-shared-noise-coverage-knobs.md`](./PROMPT-shared-noise-coverage-knobs.md). + +Copiá y pegá el bloque siguiente en una sesión **nueva**. +Roadmap: [`shared-noise-common-mode.md`](./shared-noise-common-mode.md) (2026-08-26). + +**Modo de trabajo:** **serial** — una sesión / un slice. No lanzar agentes en paralelo. +Empezá por **Slice 1** salvo que el humano diga otro. + +--- + +```text +Usando dev-protocol, ejecutá el roadmap Shared noise common-mode. + +## Contexto +Repo: VHectorLab 3D / vhectorlab (Python/uv + Vite/Three.js). +Base: `main` actualizado (git pull first si aplica). +Roadmap canónico — leelo COMPLETO antes de cualquier código: + `roadmap/shared-noise-common-mode.md` +Lecciones: `.agents/skills/dev-protocol/lessons-learned.md` + — §2 z-score/tanh (NO uses t normalizado para esta métrica; valores crudos del embedding visualizado); + §4.8 COMPARE groups; §4.11b group paint (hoy G1↔G2 — este epic lo parte); + §4.11c Group hue encima sigue igual; §7 SemVer. +Módulos del protocolo: SKILL.md + code-design.md (TDD, deep module, vertical slice) + + documentation.md (sync condicional) + git-workflow.md (approval gate). +UI viva: `src/main.js`, `src/ui/*`, `src/visualizer/*`. No reintroducir `/v25/` ni `/amiga/`. + +## Decisiones YA CERRADAS (no re-preguntar) +- Shared noise pregunta: “¿esta dim se parece en TODOS los tokens del Compare actual?” +- NO es acuerdo de centros de grupo. groupId no entra en la métrica. +- Por dim: min y max de item.embedding[d]; sim = sharedNoiseSimilarity(min, max); 0 cuenta como +. +- Un token outlier bloquea el hide temprano (por eso min/max, no más promedios). +- Slider/toggles/persistencia existentes (sameSignCancel*). Default OFF, coverage 30%. +- Solo paint → negro; Y, X, dim-sort, ruler, hover crudo: no tocar. +- Label sigue “Shared noise”. Mover el bloque a después de Zero coverage, fuera de Group contrast. +- Gate Shared noise: Compare + ≥2 tokens con embedding. Sign conflict / Group hue: siguen ≥2 grupos. +- Sign conflict sigue G1 vs G2 (primeros dos groupIds). Fuera de alcance N-grupos. +- ANALYSIS+NAV, POINTS+RIBBONS. Galaxy no-op. Arithmetic fuera. +- Fuera: cancel por hilo, segundo slider “dentro del grupo”, colapsar X, backend/SAE train. +- SemVer: PATCH 2.4.2 → 2.4.3 al ship, salvo que el humano pida MINOR. + +## Slice a ejecutar en ESTA sesión +Slice 1 — Engine: token-batch metric + split paint. + +Si el humano indica Slice 2, hacé SOLO ese. +No adelantar el slice siguiente “por si acá”. + +### Definition of Done — Slice 1 +1. Helper(s) puros: hasEnoughTokensForSharedNoise + computeTokenSharedNoiseMetrics (nombre libre si el módulo queda profundo). TDD RED→GREEN en tests/groupDimContrast.test.js (o archivo hermano). +2. Casos mínimos del roadmap §3.3 y §5: <2 tokens → []; 1 grupo sí calcula; 3 grupos usa todos los tokens (G3 outlier impide sim alta); signos mezclados → cancel 0. +3. paintWeightsForDim / applyGroupDimPaint / buildPointGroupPaintAttributes: cancel lee métrica TOKEN; highlight sigue métrica GROUP (computeDimRelationMetrics intacta). +4. Instancer (y RIBBONS path) pasa ambas métricas. No reutilizar meanA/meanB para shared noise. +5. npm test verde. Sin cambios Python salvo error real. +6. Branch: feat/shared-noise-common-mode +7. APPROVAL GATE: cómo probar en Compare con Shared noise ya ON (2 grupos alcanza para ver paint; el gate UI de 1 grupo es Slice 2). ESPERÁ OK explícito. No push/merge. + +### Definition of Done — Slice 2 (solo si el humano lo pide en esta sesión) +1. Mover markup Shared noise a después de Zero coverage; Group contrast = Sign conflict + Group hue. +2. Gate independiente ≥2 tokens (main.js + VisualizationControls). Sign conflict/hue siguen hasGroupsForDimContrast. +3. fieldInfo EN; tests de enable/disable. +4. CONTEXT.md, CHANGELOG, lessons-learned §4.11b; bump PATCH 2.4.3 (manifest, package.json, Navbar, FastAPI) al entregar. +5. Smoke roadmap §8. APPROVAL GATE → esperar OK → git-workflow.md §3. + +## Cómo probar (mínimo, Slice 1) +1. Dev server. COMPARE con varios tokens. +2. Shared noise ON, Similarity 30%: bandas chatas entre tokens se apagan; dims de signo mezclado no. +3. Tres GROUP_*: dim donde G1≈G2 y G3 al contrario NO se apaga. +4. Sign conflict ON sigue marcando opuestos G1↔G2. +5. Geometría Y igual con el toggle ON/OFF. + +## Estilo +No-fluff. TDD en helpers puros. Módulo profundo. No expandir scope. +Ante duda de contrato: preguntá — no adivines. +Ciclo SKILL.md: clarify (solo si algo contradice D1–D13) → branch → implement → verify → docs (Slice 2) → approval gate → git delivery tras OK. +``` + +--- + +## Notas para el humano + +1. Pegá el prompt en sesión limpia con `main` al día. +2. **Slice 1** = el cambio de verdad (qué dims se apagan). **Slice 2** = mover el control y poder usarlo sin `GROUP_*`. +3. Después de Slice 1: Compare con Shared noise ON y 2+ grupos ya muestra la métrica nueva (aunque el control siga dentro de Group contrast hasta Slice 2). +4. OK explícito (“ok”, “dale”, “mergealo”) para push/merge. Un thumbs-up a otra cosa no cuenta. diff --git a/roadmap/archivo/PROMPT-shared-noise-coverage-knobs.md b/roadmap/archivo/PROMPT-shared-noise-coverage-knobs.md new file mode 100644 index 0000000000000000000000000000000000000000..329e0de68b81bc2d89887ae9606ce568fb9b6201 --- /dev/null +++ b/roadmap/archivo/PROMPT-shared-noise-coverage-knobs.md @@ -0,0 +1,117 @@ +# Prompt — agente Shared noise ship + coverage A/mA knobs + +> **Archivado:** 2026-08-26 — shipped **2.4.3**. No usar para trabajo nuevo. +> Roadmap: [`shared-noise-coverage-knobs.md`](./shared-noise-coverage-knobs.md). +> Predecessor: [`shared-noise-common-mode.md`](./shared-noise-common-mode.md). + +Copiá y pegá el bloque siguiente en una sesión **nueva**. +Roadmap: [`shared-noise-coverage-knobs.md`](./shared-noise-coverage-knobs.md) (2026-08-26). +Predecessor archivado (parcial): [`shared-noise-common-mode.md`](./shared-noise-common-mode.md). + +**Modo de trabajo:** **serial** — una sesión / un slice. No agentes en paralelo. +Empezá por **Slice 1** salvo que el humano diga otro. + +--- + +```text +Usando dev-protocol, ejecutá el roadmap Shared noise ship + coverage A/mA knobs. + +## Contexto +Repo: VHectorLab 3D / vhectorlab (Python/uv + Vite/Three.js). +Base: `main` actualizado (git pull first si aplica). +Roadmap canónico — leelo COMPLETO antes de cualquier código: + `roadmap/shared-noise-coverage-knobs.md` +Predecessor (solo contexto histórico / D1–D13 métrica): + `roadmap/archivo/shared-noise-common-mode.md` +Lecciones: `.agents/skills/dev-protocol/lessons-learned.md` + — §2 z-score/tanh (métrica Shared noise = valores crudos del embedding visualizado, NO t normalizado); + §4.8 COMPARE groups; §4.11b group paint; §4.11c Group hue; §7 SemVer. +Módulos: SKILL.md + code-design.md + documentation.md + git-workflow.md. +UI viva: `src/main.js`, `src/ui/*`, `src/visualizer/*`, `src/theme/`. No `/v25/` ni `/amiga/`. + +## WIP bootstrap (OBLIGATORIO antes de codear) +1. `git status` + `git branch` + `git diff --stat`. +2. Si hay cambios de engine/100%/input editable: ponelos en rama + `feat/shared-noise-coverage-knobs` (o continuá `feat/shared-noise-common-mode`). + No los tires. Conservá techo 100%. +3. Si el árbol está limpio: re-implementá Slice 1 desde el roadmap (TDD). +4. No asumas que Slice 1 ya mergeó a main — probablemente no. + +## Decisiones YA CERRADAS (no re-preguntar) +### Métrica Shared noise +- Pregunta: ¿esta dim se parece en TODOS los tokens del Compare? +- groupId no entra; min/max por dim; sharedNoiseSimilarity; 0 cuenta como +. +- Solo paint; persist sameSignCancel* / zero coverage keys; default OFF + 30%. +- Sign conflict sigue G1↔G2. Galaxy no-op. Arithmetic fuera. + +### UI Shared noise (Slice 3) +- Bloque después de Zero coverage, fuera de Group contrast. +- Gate ≥2 tokens con embedding. Conflict/hue ≥2 grupos. + +### Coverage A/mA knobs (Slice 2) — Zero coverage Y Shared noise +- Techo 30…100% (C1). c=1 permitido en paint/remap. +- Un solo % persistido; knobs = proyección. +- A = enteros (amperes); mA = décimas 0.0…0.9 (milliamperes). +- Readout único editable; 100→A100/mA0; 99.9→A99/mA0.9. +- Clamp least-effort en el borde. +- Knob = slider disfrazado (NO drag angular real). +- Lerp visual corto del ángulo al editar readout. +- Labels A / mA + tips (i) cortos claros EN. +- Estilo Magic Workbench 3.0 elegante; knob grande + chico al costado. +- Layout barato: readout + knobs; mobile side-by-side si cabe. +- Reemplazar el experimento “input numérico + slider largo”. + +### Docs / voz +- CHANGELOG + mensaje de commit: nota EN autoirónica (roadmap T1/T2) — + studio gear cosplay on Workbench; shipping anyway vs one bad top-end slider. +- SemVer PATCH 2.4.2 → 2.4.3 al ship (salvo que el humano pida MINOR). + +## Slice a ejecutar en ESTA sesión +Slice 1 — Engine token-batch + techo 100% (verificar WIP o implementar). + +Si el humano indica Slice 2 o 3, hacé SOLO ese. +No adelantar el slice siguiente. + +### DoD — Slice 1 +1. hasEnoughTokensForSharedNoise + computeTokenSharedNoiseMetrics (TDD). +2. Casos: <2 tokens []; 1 grupo ok; 3 grupos usa todos (outlier bloquea); mixed → cancel 0. +3. paintWeightsForDim(token, group, settings): cancel←token; highlight←group. +4. Instancer/POINTS/RIBBONS pasan ambas métricas. +5. HIGH_COVERAGE_MAX=100; cancel/remap aceptan c=1. +6. npm test verde. Sin Python salvo error real. +7. APPROVAL GATE + cómo probar. ESPERÁ OK. No push/merge. + +### DoD — Slice 2 (solo si lo piden) +1. Helpers compose/decompose A+mA ↔ percent (TDD). +2. Knobs Workbench en Zero coverage + Shared noise; readout editable sync + lerp. +3. Tips fieldInfo A/mA; quitar UX rechazada del input+slider dominante. +4. npm test verde. APPROVAL GATE. No push/merge. + +### DoD — Slice 3 (solo si lo piden) +1. Mover Shared noise tras Zero coverage; Group contrast = conflict + hue. +2. Gate ≥2 tokens (main.js + VisualizationControls). +3. CONTEXT, CHANGELOG (con T2), lessons §4.11b; bump 2.4.3. +4. Smoke roadmap §6. APPROVAL GATE → OK → git-workflow.md §3 + (commit message incluye la nota T2). + +## Cómo probar (mínimo, Slice 1) +1. Dev server. COMPARE con ≥2 GROUP_* (gate UI de 1 grupo es Slice 3). +2. Shared noise ON ~30%: bandas chatas se apagan; signos mezclados no. +3. Tres grupos: G1≈G2 y G3 al contrario → dim NO se apaga. +4. Sign conflict sigue G1↔G2. Y igual ON/OFF. + +## Estilo +No-fluff. TDD. Módulo profundo. No expandir scope. +Ante duda de contrato: preguntá — no adivines. +Ciclo: clarify (solo si contradice decisiones cerradas) → branch → implement → verify + → docs (Slice 3) → approval gate → git delivery tras OK. +``` + +--- + +## Notas para el humano + +1. Pegá el prompt en **chat nuevo** con el repo a mano (el WIP puede estar sin commit en el working tree). +2. **Slice 1** = métrica. **Slice 2** = knobs A/mA. **Slice 3** = mover control + gate + ship. +3. OK explícito (“ok”, “dale”, “mergealo”) para push/merge. Un thumbs-up a otra cosa no cuenta. +4. La nota de “seriedad / studio gear” va en **commit + CHANGELOG** al ship (Slice 3). diff --git a/roadmap/archivo/PROMPT-visualization-controls.md b/roadmap/archivo/PROMPT-visualization-controls.md new file mode 100644 index 0000000000000000000000000000000000000000..3cb75d60b1cdf34e71d5d4fe6f17fc843be45448 --- /dev/null +++ b/roadmap/archivo/PROMPT-visualization-controls.md @@ -0,0 +1,66 @@ +# Prompt — agente Visualization Controls (Sign filter + color anchors) + +Copiá y pegá el bloque siguiente en una sesión nueva. **Todas las decisiones de producto ya están cerradas** en `roadmap/visualization-sign-filters-colors.md` (2026-08-02). + +--- + +```text +Usando dev-protocol, ejecutá el roadmap Visualization Filters & Color Anchors. + +## Contexto +Repo: VectorLab 3D (Python/uv + Vite/Three.js). +Base: `main` actualizado (pull first). +Roadmap canónico (leelo COMPLETO antes de codear): + `roadmap/visualization-sign-filters-colors.md` +Lecciones: `.agents/skills/dev-protocol/lessons-learned.md` + (§4.5 sliders/dock derecho, §2.2 rampa divergente, §7 SemVer, §1 WebGL). + +## Decisiones YA CERRADAS (no re-preguntar) +- V1: Panel nuevo debajo de “3D Spatial Controls” en right dock (mismo glass). +- V2: Efectivo en ARITHMETIC + COMPARE. +- V3: POINTS + RIBBONS only. +- F1: + = >0, − = <0, near-zero |t|<0.01 (ε alineado a DivergentShading). +- F2: “+ only” oculta − y near-zero; “− only” oculta + y near-zero. +- F3: Radio segmentado All | + Only | − Only (default All). +- F4: Filtrar TODA la geometría (points + continuity lines + wide ribbons). +- C1: Tres hex anclas REEMPLAZAN la rampa fija (lerp 0↔+1 y 0↔−1; sin midstops de producto). +- C2: Hex libre; C3 defaults #FFE600 / #000000 / #9900E6; C4 localStorage. +- D1: Global (no per MODE|VIEW|RENDER); D2: Reset a defaults + All. +- X1: Sin backend; X2: sin presets con nombre / export. +- Filtrar sobre activación NORMALIZADA (post z-score/tanh), no raw suelto — documentalo. +- POINTS shader: uniforms para colores (no hardcode GLSL ramp). + +## Objetivo +UI + pipeline de color/filtro según roadmap §0–§7. TDD en helpers puros. MINOR version bump (§7). + +## Fuera de alcance +- roadmap/archivo/**, backend, MESH, named palettes, per-context viz overrides. + +## Flujo +1. Branch `feat/visualization-sign-color-controls` (or staged A/B/C as in roadmap §9). +2. Implement defaults/persistence + pure filter/color tests (RED→GREEN). +3. UI panel + wire into main.js right dock below ThreadSliders. +4. Instancer/MeshFactory/DivergentShading: apply filter + anchors on POINTS and RIBBONS. +5. CHANGELOG + CONTEXT terms + short lessons note; bump version (check current manifest). +6. Vitest green + smoke checklist in roadmap §6–§7. +7. APPROVAL GATE — report how to test; wait for explicit OK before push/merge. + +## Criterios de aceptación +Ver checklist §7 del roadmap (todos los ítems). + +## Cómo probar +1. `npm run dev` (+ backend). +2. Right dock → Visualization: toggle All / + Only / − Only on POINTS then RIBBONS (Arithmetic + Compare). +3. Edit +1/0/−1 hex; confirm live recolor; reload → persisted; Reset → defaults. +4. Confirm lines/ribbons also lose filtered-sign geometry (not points-only). + +## Estilo +No-fluff; deep modules; don’t expand scope; don’t re-ask closed IDs. +``` + +--- + +## Notas para el humano + +- Decisiones V/F/C/D/X ya cerradas — pegá el prompt directo. +- Si `feat/compare-group-labels` u otros no están en `main`, mergeá antes o pedile al agente que parta de `main` limpio. diff --git a/roadmap/archivo/README.md b/roadmap/archivo/README.md new file mode 100644 index 0000000000000000000000000000000000000000..62db4089127d1f419a89c3745b40d89cd142420c --- /dev/null +++ b/roadmap/archivo/README.md @@ -0,0 +1,8 @@ +# Archivo de roadmaps + +> **Archivado:** 2026-08-02 (índice; entradas individuales llevan su propia fecha) +> Documentos de planificación ya ejecutados o superados. Cada archivo lleva al inicio la fecha de archivo y las ramas / versiones implementadas. + +No editar salvo corrección histórica. Para trabajo nuevo, crear un roadmap vivo fuera de esta carpeta o un issue/PR. + +**Reciente (2026-08-26):** `shared-noise-coverage-knobs.md` + prompt — shipped **2.4.3** (token-batch Shared noise + A/mA knobs). Predecessor parcial: `shared-noise-common-mode.md`. diff --git "a/roadmap/archivo/Shading Divergente por Activaci\303\263n.md" "b/roadmap/archivo/Shading Divergente por Activaci\303\263n.md" new file mode 100644 index 0000000000000000000000000000000000000000..0f86c0947d38e3bba855c5068b27d8a3d49037eb --- /dev/null +++ "b/roadmap/archivo/Shading Divergente por Activaci\303\263n.md" @@ -0,0 +1,289 @@ +> **ARCHIVO HISTÓRICO** — archivado 2026-08-02. +> **Estado al archivar:** shading divergente + z-score + rampas simplificadas en `main`. +> **Ramas implementadas (relacionadas):** +> - `feat/shading-divergente` +> - `feat/zscore-divergent-shading` +> - `feat/dual-color-ramps` +> - `feat/simplified-divergent-ramps` +> - `fix/arithmetic-divergent-shading` / `fix/solid-circular-points` / `fix/reduce-point-brightness` +> +> Spec de planificación de colormap; la implementación vive en `src/visualizer/DivergentShading.js`. + + +# 🗺️ Roadmap de Desarrollo: Thread Geometry, Spatial Sliders & Divergent Shading 3D + +> **Instrucciones para el Agente de IA:** +> Construir un prototipo aislado e hiper-enfocado en WebGL (Vite + Vanilla JS + Three.js) que integre dos capacidades fundamentales: +> 1. **3 Sliders de Control Espacial:** Separación Lateral ($X$), Escala Longitudinal ($Z$) y Grosor Visual. +> 2. **Shading Divergente por Activación:** Renderizado cromático por GPU (GLSL Shaders) y CPU donde: +> * $v > 0$ (Positivo) $\rightarrow$ Transición de Negro a **Rojo** (`#FF0000`). +> * $v = 0$ (Cero) $\rightarrow$ **Negro casi transparente** ($\alpha \approx 0.05$). +> * $v < 0$ (Negativo) $\rightarrow$ Transición de Negro a **Violeta** (`#8C00E6`). +> * Opacidad/Brillo dinámico modulado por la magnitud $|v|^{1.2}$. + +--- + +## 🎯 1. Definición Estricta de Alcance (In Scope vs. Out of Scope) + +### ✅ Dentro del Alcance (IN SCOPE) +* **Frontend:** Vite + Vanilla JavaScript (ES Modules) + Three.js. +* **Escena 3D:** Cámara en perspectiva, luces básicas, grilla de referencia y controles de órbita/cámara. +* **Datos Sintéticos:** Generador local de $N$ hilos vectoriales sintéticos con posiciones $(x, y, z)$ y valores de activación $v \in [-1.0, 1.0]$. +* **Módulo de Shading Divergente (`DivergentShading.js`):** + * Custom GLSL `ShaderMaterial` para GPU (Vertex + Fragment Shader). + * Función auxiliar pura en JavaScript para cálculo de color/opacidad en CPU. +* **UI Component (`ThreadSliders.js`):** + * Slider 1: `Thread Spacing (X)` (Rango: `0.1` a `10.0`, paso `0.1`). + * Slider 2: `Thread Width (Z)` (Rango: `0.1` a `5.0`, paso `0.1`). + * Slider 3: `Thread Thickness` (Rango: `1.0` a `10.0`, paso `0.5`). +* **Motor Espacial (`LayoutEngine.js`):** Mutación directa de arreglos `Float32Array` en GPU sin reconstruir geometrías. + +### ❌ Fuera del Alcance (OUT OF SCOPE - NO IMPLEMENTAR) +* **NO** Backend Python, FastAPI, ni servidores remotos. +* **NO** Carga de archivos PDF, vectores de embeddings reales ni bases de datos (LanceDB). +* **NO** Aritmética vectorial ($A - B + C$), búsqueda semántica ni menú `setup.sh` complejo. +* **NO** Autoencoders Dispersos (SAE) ni API de Gemini/OpenAI. + +--- + +## 📁 2. Estructura de Archivos del Proyecto + +```text +thread-shading-lab/ +├── index.html +├── package.json +├── vite.config.js +├── src/ +│ ├── main.js <-- Orquestador de arranque +│ ├── style.css <-- Estilos mínimos para layout y sliders +│ ├── engine/ +│ │ └── Scene.js <-- Inicialización de Three.js (Canvas, Cámara, Luces) +│ ├── visualizer/ +│ │ ├── DivergentShading.js <-- GLSL Shaders y helper CPU +│ │ ├── ThreadFactory.js <-- Creación de geometrías e intensidades de activación +│ │ └── LayoutEngine.js <-- Matemática de actualización X, Z y Grosor +│ └── ui/ +│ └── ThreadSliders.js <-- Componente aislado HTML + Event Listeners +└── tests/ + ├── DivergentShading.test.js <-- Test unitario del algoritmo de color + └── LayoutEngine.test.js <-- Test unitario de matemática espacial +``` + +--- + +## 📋 3. Fases de Implementación (Paso a Paso) + +### Fase 1: Entorno de Desarrollo y Escena 3D Base +1. **Inicializar proyecto Vite + Vanilla JS:** + * Archivo `package.json` con dependencias: `three`, `vite`, `vitest`. +2. **Crear `src/engine/Scene.js`:** + * Instanciar `THREE.Scene`, `THREE.PerspectiveCamera` (posición `(0, 5, 15)`), `THREE.WebGLRenderer` con `antialias: true`. + * Agregar un `THREE.GridHelper(50, 50)` en $Y = 0$ para tener referencia espacial. + * Configurar el loop de renderizado continuo mediante `requestAnimationFrame`. + +--- + +### Fase 2: Módulo de Shading Divergente (`DivergentShading.js`) +1. **Crear `src/visualizer/DivergentShading.js`:** + * Implementar la función de cálculo CPU `getDivergentColor(val, absMax)`: + ```javascript + export function getDivergentColor(val, absMax = 1.0) { + const denom = absMax > 1e-9 ? absMax : 1.0; + let t = Math.max(-1.0, Math.min(1.0, val / denom)); + const absT = Math.abs(t); + const alpha = Math.min(Math.max(Math.pow(absT, 1.2), 0.05), 1.0); + + let r = 0.0, g = 0.0, b = 0.0; + if (t >= 0) { + r = t; // Positivo: Negro -> Rojo (1.0, 0.0, 0.0) + } else { + const f = -t; + r = 0.55 * f; // Negativo: Negro -> Violeta (0.55, 0.0, 0.9) + b = 0.9 * f; + } + return { r, g, b, alpha }; + } + ``` +2. **Escribir Shaders GLSL para GPU:** + * **Vertex Shader:** + ```glsl + attribute float intensity; + varying float vIntensity; + uniform float pointSize; + + void main() { + vIntensity = intensity; + vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); + float dist = length(mvPosition.xyz); + gl_PointSize = clamp(pointSize * (300.0 / dist), 2.0, 60.0); + gl_Position = projectionMatrix * mvPosition; + } + ``` + * **Fragment Shader:** + ```glsl + varying float vIntensity; + uniform float baseOpacity; + + void main() { + vec2 coord = gl_PointCoord - vec2(0.5); + if (length(coord) > 0.5) discard; // Círculo perfecto + + float t = clamp(vIntensity, -1.0, 1.0); + float absT = abs(t); + float dynamicAlpha = clamp(pow(absT, 1.2), 0.05, 1.0) * baseOpacity; + + vec3 color = vec3(0.0); + if (t >= 0.0) { + color = vec3(t, 0.0, 0.0); // Positivo: Rojo + } else { + float f = -t; + color = vec3(0.55 * f, 0.0, 0.9 * f); // Negativo: Violeta + } + + gl_FragColor = vec4(color, dynamicAlpha); + } + ``` +3. **Exportar la función `createDivergentMaterial(pointSize)`** que retorne un `THREE.ShaderMaterial` configurado con `transparent: true`, `depthWrite: false` y `blending: THREE.NormalBlending`. + +--- + +### Fase 3: Fábrica de Hilos Sintéticos con Activación (`ThreadFactory.js`) +1. **Generar datos sintéticos enriquecidos con valores de activación:** + * Crear la función `createSyntheticThreads(count = 5, pointsPerThread = 100)`: + ```javascript + { + id: 0, + label: "Vector 0", + rawValues: [ /* 100 floats sintéticos entre -1.0 y 1.0 */ ], + lineMesh: null, // THREE.LineSegments + pointsMesh: null // THREE.Points con ShaderMaterial + } + ``` +2. **Asignar atributos de Buffer a la Geometría:** + * Crear `positions` (`Float32Array` de tamaño `pointsPerThread * 3`). + * Crear `intensities` (`Float32Array` de tamaño `pointsPerThread`) asignando directamente el valor de activación sintético $v \in [-1.0, 1.0]$. + * Asignar `geometry.setAttribute('intensity', new THREE.BufferAttribute(intensities, 1))`. +3. **Construir Mallas:** + * Usar `createDivergentMaterial()` para la malla de puntos (`THREE.Points`). + * Usar `THREE.LineBasicMaterial` con colores de vértices (`vertexColors: true`) calculados vía `getDivergentColor()` para la estructura de la línea. + * **REGLA CRÍTICA:** Asignar `frustumCulled = false` a las mallas de líneas y puntos. + +--- + +### Fase 4: Motor Espacial y Sliders (`LayoutEngine.js` & `ThreadSliders.js`) +1. **Implementar `updateAllThreadPositions(threads, config)` en `LayoutEngine.js`:** + * Recorrer cada hilo $i$ y aplicar offset horizontal $X = i \times \text{config.threadSpacing}$. + * Recorrer el atributo `position` de cada punto $p$ y escalar la profundidad $Z$: + $$\text{posZ}_p = p \times \text{pointSpacing} \times \text{config.threadWidth}$$ + * Indicar actualización a la GPU: `positionsAttribute.needsUpdate = true` y `geometry.computeBoundingSphere()`. + * Actualizar el tamaño del shader en la GPU: + ```javascript + pointsMesh.material.uniforms.pointSize.value = config.threadThickness * 1.5; + ``` +2. **Crear `src/ui/ThreadSliders.js`:** + * Exportar `threadSlidersMarkup()` con los 3 sliders HTML: + * Separación ($X$) + * Longitud ($Z$) + * Grosor (Puntos) + * Exportar `wireThreadSliders(threads, config)` conectando los eventos `oninput` con `updateAllThreadPositions()`. + +--- + +## ⚠️ 4. Lecciones Aprendidas e Invariantes Técnicas (OBLIGATORIO) + +El agente de IA **DEBE** aplicar rigurosamente las siguientes reglas para prevenir bugs de renderizado WebGL: + +### 1. Desactivación de Frustum Culling (`frustumCulled = false`) +* **Problema:** Three.js descarta objetos fuera de la vista usando una esfera delimitadora (`boundingSphere`) calculada al inicio. Al modificar la escala $Z$ o separar en $X$, los hilos pueden desaparecer repentinamente si la cámara gira o se acerca. +* **Solución Obligatoria:** Fijar `frustumCulled = false` en todas las mallas: + ```javascript + lineMesh.frustumCulled = false; + pointsMesh.frustumCulled = false; + ``` + +### 2. Configuración de Transparencia y Blending en Shaders +* **Problema:** Puntos con opacidad dinámica baja pueden solaparse de forma opaca si la memoria de profundidad (*depth buffer*) bloquea los píxeles traseros. +* **Solución Obligatoria:** Desactivar escritura de profundidad e integrar transparencia limpia en el `ShaderMaterial`: + ```javascript + transparent: true, + depthWrite: false, + blending: THREE.NormalBlending + ``` + +### 3. Mutación de Buffers In-Situ (Cero Re-creación de Geometría) +* **Problema:** Re-crear objetos `new THREE.BufferGeometry()` o `new THREE.Mesh()` en cada movimiento del slider causa fugas de memoria (*memory leaks*) y congelamientos en el navegador. +* **Solución Obligatoria:** Mutar el arreglo `Float32Array` existente e informar a la GPU: + ```javascript + const positions = geometry.attributes.position; + // Modificar positions.array in-situ + positions.needsUpdate = true; + geometry.computeBoundingSphere(); + ``` + +### 4. Limitaciones de Grosor de Líneas en WebGL (`LineBasicMaterial`) +* **Problema:** En muchas plataformas y navegadores (especialmente sobre ANGLE/Windows/macOS), la especificación de WebGL limita el grosor de `LineBasicMaterial.linewidth` a máximo **1 píxel**, ignorando valores mayores. +* **Solución Obligatoria:** El slider de "Grosor" debe controlar el tamaño de los puntos (`PointsMaterial.size`), los cuales sí escalan correctamente en GPU con `sizeAttenuation: true`. + +--- + +## 🧪 5. Pruebas y Verificación + +### Test Unitario 1: Algoritmo de Shading (`tests/DivergentShading.test.js`) +```javascript +import { describe, it, expect } from 'vitest'; +import { getDivergentColor } from '../src/visualizer/DivergentShading.js'; + +describe('Divergent Shading CPU Algorithm', () => { + it('debe mapear valores positivos a Rojo con opacidad proporcional', () => { + const { r, g, b, alpha } = getDivergentColor(1.0, 1.0); + expect(r).toBeCloseTo(1.0, 5); + expect(g).toBe(0.0); + expect(b).toBe(0.0); + expect(alpha).toBeCloseTo(1.0, 5); + }); + + it('debe mapear el valor cero a Negro con opacidad mínima (~0.05)', () => { + const { r, g, b, alpha } = getDivergentColor(0.0, 1.0); + expect(r).toBe(0.0); + expect(g).toBe(0.0); + expect(b).toBe(0.0); + expect(alpha).toBeCloseTo(0.05, 5); + }); + + it('debe mapear valores negativos a Violeta', () => { + const { r, g, b } = getDivergentColor(-1.0, 1.0); + expect(r).toBeCloseTo(0.55, 5); + expect(g).toBe(0.0); + expect(b).toBeCloseTo(0.9, 5); + }); +}); +``` + +### Test Unitario 2: Matemática de Disposición Espacial (`tests/LayoutEngine.test.js`) +```javascript +import { describe, it, expect } from 'vitest'; + +describe('LayoutEngine Math', () => { + it('debe calcular correctamente las posiciones X y Z', () => { + const index = 2; + const spacingX = 3.0; + const posX = index * spacingX; + expect(posX).toBe(6.0); + + const pointIdx = 5; + const widthZ = 1.5; + const baseZ = 0.1; + const posZ = pointIdx * baseZ * widthZ; + expect(posZ).toBeCloseTo(0.75, 5); + }); +}); +``` + +### Checklist de Verificación Manual +* [ ] Iniciar el proyecto con `npm run dev`. +* [ ] **Verificación de Colores:** Confirmar que los puntos con valores positivos se vean **rojos**, los valores cero sean **negros/casi invisibles**, y los valores negativos se vean **violetas/púrpuras**. +* [ ] **Verificación de Sliders:** + * Slider **Separación (X)**: Separa o junta los hilos horizontalmente. + * Slider **Longitud (Z)**: Estira o comprime la profundidad de los hilos. + * Slider **Grosor**: Modifica el tamaño de los puntos con shading divergente. +* [ ] **Cero Parpadeos:** Rotar la escena 3D y confirmar que ningún hilo desaparezca por frustum culling. \ No newline at end of file diff --git a/roadmap/archivo/amiga-workbench.md b/roadmap/archivo/amiga-workbench.md new file mode 100644 index 0000000000000000000000000000000000000000..275ffbdc43439678ae28f1f4632aafcf3e24d610 --- /dev/null +++ b/roadmap/archivo/amiga-workbench.md @@ -0,0 +1,250 @@ +# Amiga Workbench skin — `/amiga` (MagicWB) + +**Estado:** activo — Slices 2–6 wire listos (approval gate); Slice 7 HF Space pendiente de OK +**Protocolo:** `.agents/skills/dev-protocol/` (ciclo completo + approval gate) +**Prompt agente:** [`PROMPT-amiga-workbench.md`](./PROMPT-amiga-workbench.md) +**SemVer:** línea **`2.4.x`** (actual **2.4.1**) +**HF Space público:** **solo al final** (§7 Slice final) — no en slices intermedios + +--- + +## 0. Norte + +Bandera blanca **`/amiga/`**: UI paralela con estética **Commodore Amiga Workbench 3.0 + Magic Workbench** (tipografía pixel / efecto serrucho + paleta MagicWB). + +- **No** es un reskin del legado `/`. +- **No** es v25 ni continúa `gui-art` / `gui-art-v25`. +- Canvas 3D **no se toca** en este epic (clear color / shaders / POINTS / RIBBONS quedan como el engine actual cuando se cablee). +- Profundidad de este epic = **Q2-A**: tipografía + paleta + chrome mínimo legible. + **Q2-C** (cosplay full: iconos MagicWB, drawers, pointer, metaphors) → roadmap **aparte**, después de que `/amiga` esté usable. + +--- + +## 1. Decisiones cerradas (Q1–Q10) + +| ID | Decisión | +| :--- | :--- | +| **Q1** | Bandera blanca **`/amiga/`**. Legado `/` intacto. **`src/v25/**` y `v25/**` — PROHIBIDO tocar (explícito; ver lessons §6.0e). | +| **Q2** | **A** — tipografía pixel + paleta MagicWB (+ chrome 2D mínimo para que se lea como WB). **C** (cosplay) → epic futuro aparte. | +| **Q3** | **A** — no artear el look 3D; canvas héroe reusa engine sin “pixelizar” puntos/ribbons. | +| **Q4** | **A** — self-host Topaz (`.ttf` / `.woff2` en repo bajo `amiga/` o `src/amiga/assets/`). Sin CDN GitHub raw. | +| **Q5** | **A** — CSS estático (tokens + clase raíz). **No** inyección dinámica de `