Phase 0 baseline: FastAPI+SQLite operator pipeline, React/three.js UI, ffprobe/ffmpeg_frames/blender_convert operators
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
d0569dc746
11
.claude/launch.json
Normal file
11
.claude/launch.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "modelbeast",
|
||||
"runtimeExecutable": "/opt/homebrew/bin/uv",
|
||||
"runtimeArgs": ["run", "uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8777"],
|
||||
"port": 8777
|
||||
}
|
||||
]
|
||||
}
|
||||
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"WebSearch"
|
||||
]
|
||||
}
|
||||
}
|
||||
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
# Runtime data (assets, jobs, db) — never commit
|
||||
data/
|
||||
|
||||
# Python
|
||||
.venv/
|
||||
venvs/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.python-version
|
||||
|
||||
# Node / frontend build
|
||||
web/node_modules/
|
||||
web/dist/
|
||||
|
||||
# Secrets & local config
|
||||
*.env
|
||||
.env
|
||||
server/.env
|
||||
|
||||
# Large model weights / caches
|
||||
models/
|
||||
*.safetensors
|
||||
*.ckpt
|
||||
*.pt
|
||||
|
||||
# Compiled native helpers (source is committed, binary is not)
|
||||
bin/objcap
|
||||
bin/*.o
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
171
HANDOFF.md
Normal file
171
HANDOFF.md
Normal file
@ -0,0 +1,171 @@
|
||||
# MODELBEAST — Build Handoff Brief
|
||||
|
||||
**Audience:** the AI agent (Opus 4.8 / Claude Code) continuing this build. Read this file top to bottom, then read `PLAN.md`, then start at Phase 1. Do not re-research what is already verified here — every tool claim in this doc and PLAN.md was verified against primary sources on 2026-07-12 by research agents.
|
||||
|
||||
---
|
||||
|
||||
## 0. Mission and definition of done
|
||||
|
||||
MODELBEAST is a **local-first web app** on an M3 Ultra Mac Studio (256GB unified memory) that turns videos/images/3D files into meshes, splats, mocap, and rigged animated characters. Local Apple Silicon tools do the heavy lifting; cloud APIs (fal.ai, Tripo, Meshy) are optional quality tiers. The owner's priority is **capability and quality, not cost savings**. He films things, drops them in, and wants finished 3D assets out — with full parameter control at every step.
|
||||
|
||||
**Done means:** all four phases below shipped and each operator proven end-to-end through the real browser UI (not just curl), with PLAN.md/README.md kept current.
|
||||
|
||||
## 1. Current state (Phase 0 — shipped and verified 2026-07-12)
|
||||
|
||||
Working today:
|
||||
- FastAPI backend (`server/`), SQLite (`data/modelbeast.db`), job runner executing operators as subprocesses with streamed logs over WebSocket.
|
||||
- React + Vite + three.js frontend (`web/`), served from `web/dist` by FastAPI. Drag/drop/paste ingest, asset library with kind detection, GLB/OBJ/FBX/PLY/STL viewer (GLTFLoader etc. + OrbitControls + AnimationMixer), video/image/frames-grid/JSON previews, param forms auto-generated from operator JSON Schemas, live job dashboard.
|
||||
- Operators, all tested: `ffprobe` (inspect), `ffmpeg_frames` (fps sampling + mpdecimate dedupe + blur cull + max cap), `blender_convert` (universal format conversion via headless Blender).
|
||||
|
||||
Run it:
|
||||
```bash
|
||||
/opt/homebrew/bin/uv run uvicorn server.main:app --host 0.0.0.0 --port 8777
|
||||
```
|
||||
(`.claude/launch.json` has this config, name `modelbeast`.) Frontend rebuild after editing `web/src`: `cd web && npm run build`.
|
||||
|
||||
File map:
|
||||
```
|
||||
server/main.py FastAPI routes: /api/assets, /api/jobs, /api/operators, /ws, static mount
|
||||
server/db.py sqlite schema (assets, jobs), connect() — check_same_thread=False (required!)
|
||||
server/store.py asset store: data/assets/<id>/<name>, kind inference by extension
|
||||
server/registry.py scans server/operators/*/manifest.json
|
||||
server/runner.py queue + subprocess execution + output registration (result.json contract)
|
||||
server/operators/<id>/{manifest.json, run.py}
|
||||
web/src/{App.jsx, Viewer.jsx, api.js, index.css}
|
||||
data/{assets,jobs,inbox,outputs} (gitignore this whole dir)
|
||||
PLAN.md verified tool matrix + roadmap — treat as source of truth
|
||||
```
|
||||
|
||||
**Operator contract** (do not break it; extend it backward-compatibly):
|
||||
- Runner invokes: `<python> run.py --input <asset path> --outdir <job dir> --params '<json>'`.
|
||||
- Operator writes outputs into outdir. Optional `result.json`: `{"outputs": [{"path": ..., "name": ..., "meta": {...}}], "summary": {...}}`. Without it, every top-level file in outdir is registered as an asset. stdout/stderr = job log.
|
||||
- `manifest.json` fields: `id, name, category, description, accepts (asset kinds), produces, entry, params_schema (JSON Schema), python (optional absolute venv python — omit to use the server's venv)`.
|
||||
|
||||
## 2. Environment facts (do not rediscover)
|
||||
|
||||
- Machine: M3 Ultra, 256GB unified memory, macOS (Darwin 25.x). ~400GB local disk free — model weights are large; prefer `~/models/` or an external SSD if the owner has attached one by now (ask via a settings field, don't block on it).
|
||||
- Secondary machine: M4 Pro, 4TB SSD, Tailscale `100.69.21.128` (~20–80ms RTT). Cold storage/archive only — not a hot working drive.
|
||||
- Installed: ffmpeg 8.1.2, Blender 5.1.2 at `/Applications/Blender.app/Contents/MacOS/Blender`, Python 3.14 (Homebrew — TOO NEW for ML wheels, never use it for tool venvs), `uv`, node/npm, Ollama (`qwen3:235b`, `qwen3:32b`, `qwen3:8b` pulled), Epic Games Launcher + UE 5.8 at `/Users/Shared/Epic Games/UE_5.8`. BlenderMCP configured in Blender.
|
||||
- NOT installed yet: COLMAP, ComfyUI, Xcode (check `xcode-select -p`; Object Capture CLI needs full Xcode or at least Swift toolchain — verify).
|
||||
- Not a git repo yet. **First action: `git init`, write `.gitignore` (`data/`, `web/node_modules/`, `web/dist/`, `.venv/`, `*.env`, `venvs/`, `models/`), commit Phase 0 as baseline.** Commit at every working milestone.
|
||||
|
||||
**Gotchas already hit (don't repeat):**
|
||||
1. `sqlite3.connect` needs `check_same_thread=False` — FastAPI sync endpoints run in a threadpool.
|
||||
2. Embedding params into generated Blender python: JSON `true/false/null` are not Python — use `params = json.loads(<double json.dumps>)`.
|
||||
3. `.claude/launch.json` must use absolute `/opt/homebrew/bin/uv` (GUI-spawned processes don't get Homebrew PATH).
|
||||
4. Blender 5.x: several third-party add-ons (SMPL-X, Rokoko) lag on 5.x compatibility — test add-ons headless before building operators around them; fall back to driving Blender 4.x LTS via a second install if an add-on hard-fails (ask owner before installing).
|
||||
|
||||
## 3. Non-negotiable working rules
|
||||
|
||||
1. **One tool = one operator = one isolated uv venv.** ML repos have violently conflicting deps. Create venvs under `venvs/<tool>/` with `uv venv --python 3.11` (or the version the repo pins; 3.10–3.12, never 3.14), install with `uv pip install --python venvs/<tool>/bin/python`. Set `"python": "<abs path>"` in the manifest.
|
||||
2. **Verify every operator end-to-end through the browser UI** before moving on: upload real input → run with non-default params → watch log stream → confirm output asset registers and previews. Then commit.
|
||||
3. **Never block the event loop.** All heavy work stays in subprocesses. If an operator needs a long-lived model server (e.g. trellis-mac warm weights), run it as a separate process the operator talks to — see §5.3.
|
||||
4. **Fail loud, log everything.** Operators print progress lines (they stream to the UI). Exit non-zero on failure. Never swallow stderr.
|
||||
5. **Don't gold-plate the framework.** No Celery, no Redis, no Postgres, no node-graph editor yet. The SQLite + subprocess architecture is deliberate; extend it only as specified in §4.
|
||||
6. **CUDA-locked tools stay off this machine.** Do not attempt local installs of: TRELLIS v1 official, WHAM, TRAM, EasyMocap, ActionMesh, DuoMo, GEM-X, UniRig, SuGaR/2DGS, ComfyUI-3D-Pack, gsplat/splatfacto. PLAN.md documents why. Cloud/API is the path for those.
|
||||
7. Persistent memory for this project lives at `~/.claude/projects/-Users-m3ultra-Documents-MODELBEAST/memory/` — read `MEMORY.md` there at session start; keep it updated as you learn things.
|
||||
|
||||
## 4. Framework upgrades (build these as they become needed, not all upfront)
|
||||
|
||||
### 4.1 Settings + API keys (needed by Phase 1)
|
||||
- New table `settings (key TEXT PRIMARY KEY, value TEXT)`. Endpoints `GET/PUT /api/settings` (PUT accepts partial dict). Keys: `fal_key`, `tripo_key`, `meshy_key`, `replicate_token`, `models_dir`, `archive_host` (default `m4pro@100.69.21.128`), `archive_path`.
|
||||
- Runner injects settings as env vars (`FAL_KEY`, `TRIPO_KEY`, …) into operator subprocesses. Secrets never go into params/logs — redact anything matching a key value from job logs before storing.
|
||||
- Frontend: a Settings modal (gear icon in header). Password-type inputs. Show which operators are enabled/disabled by missing keys: add `"requires_env": ["FAL_KEY"]` to manifests; UI greys out operators whose requirements are unset.
|
||||
|
||||
### 4.2 Multi-input jobs (needed by Phase 3 retargeting)
|
||||
- Extend jobs: `asset_ids` JSON array (keep `asset_id` working for single-input). Runner passes `--input <path>` repeated, in order; manifest declares `"inputs": [{"name": "mesh", "accepts": ["model"]}, {"name": "motion", "accepts": ["model"]}]` for multi-input ops. UI: when such an operator is selected, render one asset-picker per declared input.
|
||||
|
||||
### 4.3 Queue lanes (needed once heavy local ops exist, Phase 1)
|
||||
- Manifest gains `"resources": "gpu" | "cpu" | "net"` (default `cpu`). Runner keeps three lanes: `gpu` concurrency 1 (Metal contention), `cpu` concurrency 3, `net` concurrency 6. Simple dict of semaphores; no framework.
|
||||
|
||||
### 4.4 Job management (Phase 1 polish)
|
||||
- `POST /api/jobs/{id}/cancel` (kill process group — spawn with `start_new_session=True`), `DELETE /api/jobs/{id}` (remove row + job dir). UI: cancel button on running jobs, retry button on failed jobs (re-POST same operator/asset/params), collapsible live log (already exists; make it auto-scroll).
|
||||
|
||||
### 4.5 Workflow presets (Phase 2+)
|
||||
- `server/workflows/<id>.json`: `{"id", "name", "description", "steps": [{"operator", "params", "input": "$source" | "$step0.output0"}]}`. Sequential executor in runner (a workflow run = chain of jobs; each step's registered assets feed the next). `GET /api/workflows`, `POST /api/workflows/{id}/run {asset_id, param_overrides}`. UI: "Workflows" section above operators — pick asset, pick workflow, override params, run; show step progress.
|
||||
- Ship presets: `video_to_splat`, `video_to_scanned_mesh`, `image_to_mesh_compare`, `image_to_rigged_character` (as its operators land).
|
||||
|
||||
### 4.6 Compare view (Phase 1)
|
||||
- The 256GB flex: fan one image across N mesh generators. Implement as a workflow that launches parallel jobs + a frontend "Compare" screen: grid of three.js viewers, one per output of the selected jobs, with operator name + wall-clock + file size captions. Keep it simple: a `?compare=jobId1,jobId2,…` route.
|
||||
|
||||
## 5. PHASE 1 — Mesh generation (local + API)
|
||||
|
||||
### 5.1 `fal_trellis`, `fal_trellis2`, `fal_hunyuan3d`, `fal_rodin` (API operators — build FIRST, they validate the whole pipeline cheaply)
|
||||
- One shared helper module `server/operators/_lib/fal_helper.py` (operators may import via relative path or sys.path insert; keep it dependency-light). Use the `fal-client` pip package in the **server's own venv** (it's tiny and conflict-free): upload input image with `fal_client.upload_file`, `fal_client.subscribe(endpoint, arguments=...)`, poll with progress prints, download resulting GLB into outdir.
|
||||
- Endpoints (verify exact IDs against https://fal.ai/3d-models before coding; prices verified 2026-07-12): `fal-ai/trellis` ($0.02), `fal-ai/trellis-2` ($0.25–0.35 by resolution), `fal-ai/hunyuan3d/v2` ($0.16 untextured / $0.48 textured), `fal-ai/hyper3d/rodin` ($0.40; HighPack $1.20), `tripo3d/tripo/v2.5` ($0.20–0.40).
|
||||
- Params to surface (per model, from fal docs): resolution/steps/seed/texture toggles — expose everything fal accepts; that's the product's point. `accepts: ["image"]`, `produces: ["model"]`, `resources: "net"`, `requires_env: ["FAL_KEY"]`.
|
||||
- Acceptance: upload a photo in the UI → run fal_trellis → GLB appears and renders in the viewer. Test with a real key from Settings.
|
||||
|
||||
### 5.2 `sf3d` (local fast draft — first local ML operator, proves the venv pattern)
|
||||
- Repo: https://github.com/Stability-AI/stable-fast-3d — **official MPS support** (needs macOS 15.2+; on 256GB ignore their low-RAM CPU advice). License: Stability Community (free under $1M revenue — fine).
|
||||
- `venvs/sf3d` with the Python the repo pins (likely 3.10/3.11). Follow their README install; HF weights need `huggingface-cli login` OR accept-terms — if weights are gated, print a clear job-log error telling the owner what to do, and add `hf_token` to settings.
|
||||
- Wrap their `run.py` (single image → GLB with UV textures). Params: `texture_resolution`, `remesh` options if exposed. `resources: "gpu"`.
|
||||
- Acceptance: same photo → mesh in seconds-to-a-minute on MPS; renders in viewer.
|
||||
|
||||
### 5.3 `trellis_mac` (local flagship — hardest install of Phase 1; budget real debugging time)
|
||||
- Repo: https://github.com/shivampkumar/trellis-mac (MIT; PyTorch-MPS port of Microsoft TRELLIS.2 with Metal kernels: mtlgemm, mtldiffrast). ~15GB weights. Benchmarked M4 Pro 24GB: ~5min cold / ~3m20s warm, 18GB peak. **No M3 Ultra numbers exist — measure and record yours in PLAN.md §4.**
|
||||
- Expect compile steps for the Metal extensions (needs Xcode CLT — present if Homebrew works). Known limits: hole-filling disabled, sparse attention ~80s bottleneck. If the repo has moved/broken, fallback: https://github.com/pedronaugusto/trellis2-apple (MLX backend).
|
||||
- Because cold-start dominates, implement **warm mode**: the operator's run.py checks for a running trellis daemon (a tiny HTTP wrapper you write, listening on 127.0.0.1:8801, loaded once); if absent, either start it and wait, or run one-shot. A `keep_warm` boolean param + a settings toggle. Don't over-engineer: single-endpoint Flask/FastAPI file inside the trellis venv.
|
||||
- Output: GLB with baked PBR. Params: resolution tier, texture size (512–2048), seed, steps.
|
||||
- Acceptance: same photo through trellis_mac vs sf3d vs fal_trellis2 in the Compare view; record timings.
|
||||
|
||||
### 5.4 (stretch) `hunyuan3d_mac` — shape-only on MPS via https://github.com/Maxim-Lanskoy/Hunyuan3D-2-Mac or the ComfyUI-free fork Brainkeys/Hunyuan3D-2.1-mac (unverified — inspect before trusting). Texture stage is CUDA-locked in the official repo; ship shape-only and note it. Skip entirely if the forks are dead — fal_hunyuan3d covers it.
|
||||
|
||||
## 6. PHASE 2 — Scan track (video → splat / mesh)
|
||||
|
||||
### 6.1 `colmap_poses`
|
||||
- `brew install colmap`. GLOMAP is merged into COLMAP as the global mapper — verify the subcommand in `colmap help` (expect `colmap global_mapper` or a `glomap` binary; the standalone repo is deprecated).
|
||||
- Input: `frames` asset. Pipeline inside run.py: feature extraction (SIFT, CPU on Mac — fine) → matching (sequential matcher for video) → global mapper → export as a **nerfstudio/colmap-format dataset folder** (`produces: ["colmap_dataset"]` — add this asset kind). Params: matcher type, downscale, camera model, quality preset.
|
||||
- Acceptance: 150–300 frame orbit video of an object (ask owner to film one, or use any test video; even a phone orbit of a chair works) → sparse reconstruction completes; log reports registered-image count.
|
||||
|
||||
### 6.2 `brush_train` (3DGS)
|
||||
- https://github.com/ArthurBrussee/brush — native Metal, has a full CLI. Prefer a release binary; else `cargo build` (install rust via brew). Input: `colmap_dataset`. Output: splat `.ply` (`produces: ["splat"]` — new kind for `.ply`/`.spz`/`.splat`; note plain mesh PLY vs splat PLY ambiguity — tag via meta, don't fight extension detection).
|
||||
- Params: iterations (7k/30k presets), sh-degree, max splats. `resources: "gpu"`.
|
||||
- Frontend: add a splat viewer — npm `@mkkellogg/gaussian-splats-3d` (three.js-based) renders splat PLYs; wire into Viewer.jsx by kind.
|
||||
- Fallback trainer if Brush misbehaves: OpenSplat (`cmake -DGPU_RUNTIME=MPS`). Speed watch: https://github.com/rayanht/msplat (pure-Metal, young).
|
||||
|
||||
### 6.3 `object_capture` (video/photos → textured mesh — the quality mesh path)
|
||||
- Apple RealityKit `PhotogrammetrySession`. Build a small Swift CLI (Apple sample: "creating a photogrammetry command line app" in developer docs) — needs Xcode; if only CLT is present, ask the owner to install Xcode (one-time). Compile once into `bin/objcap`, commit the Swift source, gitignore the binary.
|
||||
- Input: `frames` (or image folder). Output: USDZ + also run `blender_convert` logic to emit GLB. Params: detail level (reduced/medium/full/raw). macOS 26 shipped improved reconstruction — no code changes needed.
|
||||
- Acceptance: same orbit frames through object_capture vs brush_train; mesh renders textured in viewer.
|
||||
|
||||
## 7. PHASE 3 — Motion track
|
||||
|
||||
### 7.1 `freemocap`
|
||||
- `venvs/freemocap`, Python 3.10–3.12, `pip install freemocap` (v1.8+, official Mac support, AGPL — fine for internal tooling). Drive it headless (it has a CLI/python API; inspect `freemocap --help`). Input: video (monocular OK but approximate; multi-cam is its strength). Output: 3D keypoint data + Blender export via `freemocap_blender_addon` — final deliverable should be an FBX/BVH the retarget step can consume. If the Blender add-on fights Blender 5.1.2, run its export through Blender with `--factory-startup` and the add-on zip, or fall back to emitting raw keypoints + a note.
|
||||
- Set expectations in the operator description: "previz-grade from single camera; research-grade needs GVHMR (cloud)."
|
||||
|
||||
### 7.2 `gvhmr_import` (best-quality mocap, hybrid)
|
||||
- GVHMR itself is CUDA — do NOT install locally. Build the import half: accept a GVHMR results file (the owner runs the Colab/HF Space manually at first) and convert SMPL params → FBX/BVH. Tools: https://github.com/softcat477/SMPL-to-FBX or Meshcapade's SMPL Blender add-on (verify Blender 5 compat; MPI SMPL-X add-on requires free registration — flag to owner).
|
||||
- Stretch: a `gvhmr_cloud` operator that submits to a rented GPU (RunPod API) — only if the owner provides a RunPod key; otherwise document the manual Colab flow in the operator description.
|
||||
|
||||
### 7.3 `blender_retarget` (multi-input: character model + motion)
|
||||
- Uses §4.2 multi-input. Headless Blender: import rigged character (FBX/GLB) + BVH/FBX motion → retarget → export FBX/GLB with baked animation.
|
||||
- Retargeting add-on order of preference: the "Retarget" extension from extensions.blender.org (Expy Kit successor, Blender 5+ native — best bet), Rokoko plugin (free, may lag Blender 5), keemap (manual mapping files, bulletproof fallback). Prototype interactively in Blender first (BlenderMCP is available), then script it headless. This is the fiddliest operator of the project — bone-name mapping presets (Mixamo, Rigify, SMPL, MetaHuman) should be a param enum with a JSON mapping file each.
|
||||
|
||||
### 7.4 UE MetaHuman bridge (thin, don't overbuild)
|
||||
- The UE 5.8 markerless BODY plugin is **Windows-only** (verified; PLAN.md §Stage 4). Mac gets facial solve. Bridge = a watch folder: `data/inbox/` — a `watchfiles` task in the server auto-ingests any file dropped there (this is generically useful anyway). Owner exports FBX from UE (local Mac facial solves, or body solves from a Windows GPU VM) into that folder → lands as asset → feed `blender_retarget`.
|
||||
|
||||
## 8. PHASE 4 — Character factory + polish
|
||||
|
||||
- **`tripo_character` (API)**: the only end-to-end gen→T-pose→quad-retopo→auto-rig→animate pipeline (~$0.55–0.75/character, FBX out). API docs: docs.tripo3d.ai. Multi-step async API — poll task states, print progress. Params: all of it (T-pose toggle, quad, texture quality, rig, animation preset ids). This operator is the "money button" — make it robust.
|
||||
- **`meshy_rig` (API)**: cheapest rig+animate loop (rig 5cr, anim 3cr) for meshes generated elsewhere. docs.meshy.ai.
|
||||
- **Mesh2Motion bridge**: it's a local web app (MIT): `git clone https://github.com/Mesh2Motion/mesh2motion-app && npm i && npm run dev`, semi-automatic (user positions template skeleton). Don't automate it — add a "Open in Mesh2Motion" button in the asset detail view that copies the GLB path / opens the local app URL, and rely on the watch folder for the return trip.
|
||||
- **`archive_to_m4pro`**: rsync over ssh to `settings.archive_host:archive_path`, `--partial --progress`, verify with size/count, then optionally delete local. Requires the owner to have ssh keys set up — if `ssh -o BatchMode=yes <host> true` fails, print exact instructions in the job log and exit 1. Never delete local data unless `delete_local: true` param was explicitly set.
|
||||
- **LLM copilot pane** (optional, do last): chat sidebar hitting Ollama (`http://localhost:11434/api/chat`, model `qwen3:32b` default — NOT the 235b for interactive use). System prompt embeds the operator manifest list; the model suggests operator + params as JSON; a button applies the suggestion to the form. No agentic execution — suggestions only.
|
||||
- **Thumbnails**: background task renders model thumbnails via headless Blender (small turntable PNG) for the asset list. Nice-to-have.
|
||||
|
||||
## 9. Testing & quality bar
|
||||
|
||||
- `tests/smoke.sh`: starts server on a scratch port with `MODELBEAST_DATA=<tmpdir>` (add this env override to `db.py` — small change), uploads fixtures (generate with ffmpeg testsrc2 + a Blender-exported GLB, as Phase 0 did), runs ffprobe + ffmpeg_frames + blender_convert via curl, asserts statuses `done` and output assets exist. Run before every commit.
|
||||
- Each new operator: at minimum one real end-to-end browser run (screenshot-verified if you have browser tooling) + smoke-test coverage where inputs can be synthesized.
|
||||
- Failure modes to test deliberately: missing API key (operator greyed out / clean error), cancel mid-run (process actually dies — check with `ps`), server restart with a running job (job re-queues; already implemented — keep it working), garbage input file (clean error, not a hung job).
|
||||
- Performance discipline: record wall-clock + peak memory (`/usr/bin/time -l`) for every local ML operator's first successful run in a `BENCHMARKS.md` table. These are the first public M3 Ultra numbers for several of these tools — the owner cares.
|
||||
|
||||
## 10. Priority order & scope guard
|
||||
|
||||
Build order: **git init/baseline → 4.1 settings → 5.1 fal ops → 4.6 compare → 5.2 sf3d → 4.3 lanes + 4.4 job mgmt → 5.3 trellis_mac → 6.1 colmap → 6.2 brush + splat viewer → 6.3 object_capture → 4.5 workflows + presets → 7.1 freemocap → 4.2 multi-input → 7.3 retarget → 7.2 gvhmr_import → 7.4 watch folder → Phase 4 items by owner preference (ask which he wants first — tripo_character is the likely answer).**
|
||||
|
||||
If something is broken upstream (repo moved, weights gated, API renamed): timebox to ~30 minutes of fixing, then ship the operator in a clearly-disabled state (manifest `"status": "blocked"` + reason shown in UI) and move on. Momentum beats completeness; the owner would rather have 12 working operators and 2 blocked ones than 6 perfect ones.
|
||||
|
||||
Keep PLAN.md, README.md, and this file's checkboxes updated as you go. Update project memory with anything a future session must know (new gotchas, benchmark numbers, changed decisions).
|
||||
138
PLAN.md
Normal file
138
PLAN.md
Normal file
@ -0,0 +1,138 @@
|
||||
# MODELBEAST — Master Flow & Build Plan
|
||||
|
||||
*Written 2026-07-12. Every tool/claim below was verified against primary sources (repos, docs, pricing pages) by research agents on this date — including corrections to the Gemini advice that seeded this project.*
|
||||
|
||||
## Mission
|
||||
|
||||
A **local-first web app running on the M3 Ultra** (reachable over Tailscale from any device) that turns videos, images, and 3D files into meshes, splats, mocap, and rigged animated characters. Local Apple Silicon tools do the heavy lifting; cloud APIs (fal.ai, Tripo, Meshy) are optional quality/convenience tiers — not necessities. Full parameter control at every stage.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ MODELBEAST WEB UI (React + three.js)│
|
||||
│ drag/drop/paste: mp4 png jpg glb obj fbx │
|
||||
│ blend usdz ply · workflow picker · param │
|
||||
│ forms · job dashboard · 3D preview · export│
|
||||
└──────────────────┬──────────────────────────┘
|
||||
│ REST/WebSocket
|
||||
┌──────────────────▼──────────────────────────┐
|
||||
│ FastAPI ORCHESTRATOR (asset DB + job DAG)│
|
||||
└──┬──────┬──────┬──────┬──────┬──────┬───────┘
|
||||
│ │ │ │ │ │
|
||||
┌─────────▼─┐ ┌──▼───┐ ┌▼─────┐ ┌────▼───┐ ┌▼──────────┐
|
||||
│ INGEST │ │MESH │ │SCAN │ │ MOCAP │ │ RIG/ANIM │
|
||||
│ ffmpeg │ │GEN │ │3DGS+ │ │ │ │ │
|
||||
│ frames, │ │ │ │photo-│ │ │ │ │
|
||||
│ scenes, │ │ │ │gram │ │ │ │ │
|
||||
│ sharpness │ │ │ │ │ │ │ │ │
|
||||
└───────────┘ └──────┘ └──────┘ └────────┘ └───────────┘
|
||||
each stage = swappable "operators", local or API
|
||||
│
|
||||
┌──────────────────▼──────────────────────────┐
|
||||
│ CONVERT/EXPORT: Blender headless (universal│
|
||||
│ GLB/FBX/OBJ/USD/BLEND) · archive → M4 Pro │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Verified capability matrix (July 2026)
|
||||
|
||||
### Stage 1 — Video ingest & preprocessing (LOCAL, trivial)
|
||||
- **ffmpeg 8.1.2** (installed): frame extraction (`fps=2..3` for orbits), `mpdecimate` to drop near-dupes, `scdet`/PySceneDetect for scene splits, I-frame selection.
|
||||
- **Sharpness culling**: Laplacian-variance scoring per chunk; a few blurry frames can poison COLMAP matching. Target 150–300 frames/scene.
|
||||
|
||||
### Stage 2 — Mesh generation from image(s)
|
||||
**Gemini said "local is impractical, use fal.ai" — REFUTED since early 2026.**
|
||||
|
||||
| Tier | Tool | Runs | Output | Notes |
|
||||
|---|---|---|---|---|
|
||||
| **Local SOTA** | **trellis-mac** (TRELLIS.2 MPS port) github.com/shivampkumar/trellis-mac | M3 Ultra, ~18GB peak, est. ~3–5 min/gen | GLB + baked PBR, ~400K verts | The headline local capability. MIT. ~15GB weights |
|
||||
| Local fast draft | **SF3D / SPAR3D** (Stability, official MPS + Metal kernels) | seconds-fast | GLB, UV textures | Draft/iteration tier |
|
||||
| Local 2nd opinion | **Hunyuan3D 2.1 Mac forks** (Brainkeys, SimonTingle ComfyUI-Mac) | shape solid on MPS; texture stage shaky w/o CUDA | GLB/OBJ | Best open textures when it works |
|
||||
| API cheap | fal `fal-ai/trellis` | $0.02/gen | textured GLB | 2024-tier quality |
|
||||
| API strong | fal `trellis-2` $0.25–0.35 · fal `hunyuan3d/v2` $0.16–0.48 · Hunyuan 3.1 Pro $0.375+ | — | GLB/OBJ, PBR | Zero-setup SOTA |
|
||||
| **API character pipeline** | **Tripo v3 API** — the only gen→T-pose→quad retopo→auto-rig→animate chain | ~$0.55–0.75/rigged char | **FBX**, quads, rigged | The "finished game asset" button |
|
||||
| API hero assets | Rodin Gen-2 via fal `hyper3d/rodin` $0.40 (HighPack $1.20) | — | GLB/USDZ/FBX, quad, 4K | Film-grade fidelity |
|
||||
|
||||
Also open + interesting: **Hunyuan3D-Omni** (pose-conditioned generation — make the character already posed), **Hunyuan3D-Part / PartCrafter** (part decomposition), TripoSG (open shape-only). TripoSR runs on Mac but is obsolete quality.
|
||||
|
||||
### Stage 3 — Scanning: video → splat / mesh (LOCAL, strong)
|
||||
**Gemini's "Nerfstudio has strong Mac support" is wrong for splats** — gsplat/splatfacto is CUDA-only on Mac.
|
||||
|
||||
- **Poses**: COLMAP via Homebrew with the **GLOMAP global mapper** (now merged into COLMAP — much faster than incremental). Optional: colmap-metal fork for Metal SIFT.
|
||||
- **Splat training**: **Brush** (native Metal, active, best UX, splat .ply out) · **OpenSplat** (`-DGPU_RUNTIME=MPS`, the proven fallback) · **msplat** (new pure-Metal trainer, reportedly 5–6× gsplat speed — watch it).
|
||||
- **Meshes from reality**: **Apple Object Capture** (`PhotogrammetrySession`, GUI in Reality Composer Pro, or a tiny Swift CLI we compile) — native, fast, free, and the correct answer since all 3DGS→mesh research tools (SuGaR/2DGS) are CUDA-locked. macOS 26 shipped an improved reconstruction algorithm.
|
||||
- Capture front-ends: Scaniverse (free splat PLY/SPZ + mesh export), Polycam (paywalled formats).
|
||||
|
||||
### Stage 4 — Motion capture from video
|
||||
**Gemini's claims here were mostly wrong**: WHAM is CUDA-locked (name also garbled — it's "World-grounded Humans with Accurate Motion"); EasyMocap has zero Mac support; **ActionMesh is real** (Meta CVPR 2026) but CUDA-only *and not mocap* (rig-free animated mesh, no skeleton).
|
||||
|
||||
| Tier | Tool | Runs | Output |
|
||||
|---|---|---|---|
|
||||
| **Filming (face)** | **UE 5.8 MetaHuman Animator on Mac** — facial solve (offline + realtime from iPhone/webcam) is officially supported on macOS in 5.8; M3 Ultra exceeds Epic's recommended M2 Ultra tier | UE 5.8 (installed) | MetaHuman facial anim → AnimSequence/FBX |
|
||||
| **Filming (body) — VERDICT** | **UE MetaHuman Markerless Mocap plugin is WINDOWS-ONLY** (Epic docs: "available only on Windows for body or face and body capture"; Fab listing gates to Windows). Fully local, no API key, DX12-class GPU (AMD ok, no CUDA), ~1 min solve per second of 1080p60 footage, hands/fingers included, single performer, tripod required. **Mac workaround: rent a Windows GPU VM** (AWS g5/Paperspace/Shadow, DX12 + 8GB VRAM) only for solves, export FBX ("Performer Skeleton" option = retarget to non-MetaHuman rigs), pull back to Mac. Meshcapade's own cloud (meshcapade.me) was SHUT DOWN Apr 2026 post-acquisition — this plugin is the only outlet for the tech | Windows only | AnimSequence → FBX |
|
||||
| Local turnkey | **FreeMoCap v1.8** (`pip install freemocap`, official Mac support) + its Blender add-on | native | rigged armature → FBX/BVH. Best with 2–3 cameras |
|
||||
| Hybrid best-quality | **GVHMR** on Colab/HF-Space or rented RTX (~$0.30/hr) → SMPL import via CEB/Meshcapade Blender add-ons | cloud solve, local everything else | world-grounded SMPL |
|
||||
| Local previz | MediaPipe/BlendArMocap | native, realtime | rough |
|
||||
| API | Move One $18/mo · DeepMotion $17/mo · Autodesk Flow Studio (ex-Wonder Studio) $10–45/mo · Rokoko Video (free ≤15s FBX) | — | FBX/BVH |
|
||||
|
||||
Not Mac-runnable locally (CUDA-locked): WHAM, TRAM, EasyMocap, ActionMesh, GEM-X/GENMO, DuoMo.
|
||||
|
||||
### Stage 5 — Rigging & retargeting (LOCAL, free)
|
||||
- **Mesh2Motion** (MIT, runs locally via npm/Docker): template-skeleton rigging (human/quadruped/bird) + animation library, GLB in/out. *Semi-automatic* — you position the skeleton, it auto-skins.
|
||||
- **Mixamo**: still free, still works from a browser, but frozen (bipeds only).
|
||||
- **Blender 5.1.2** (installed, + BlenderMCP): Rigify baseline · **Rokoko plugin** (free retargeting) · **Retarget extension** (Expy Kit successor, Blender 5+) · keemap for exotic rigs · SMPL/SMPL-X add-ons for mocap import.
|
||||
- AI auto-rig: **UniRig** (open weights, CUDA-only → cloud job) · **Tripo rig API** (25cr) · **Meshy rig API** (5cr, cheapest).
|
||||
|
||||
### Stage 6 — LLM brain (LOCAL, already proven)
|
||||
- Ollama with **qwen3:235b already pulled**. Mid-2026 pattern: small fast MoE (Qwen3.6-35B-A3B class) for pipeline logic/tool-calling + the big model on demand. LM Studio/MLX when MLX outruns Ollama.
|
||||
- Used inside the app for: workflow suggestion, prompt-writing for gen models, batch naming/tagging, "explain this failure".
|
||||
|
||||
---
|
||||
|
||||
## 2. The MODELBEAST app
|
||||
|
||||
### Architecture decisions (defaults chosen, all swappable)
|
||||
| Concern | Choice | Why |
|
||||
|---|---|---|
|
||||
| Backend | **FastAPI** (Python 3.12 via `uv`) | All ML tooling is Python; async job streaming via WebSocket |
|
||||
| Tool isolation | **each operator = subprocess in its own uv venv** | These repos have violently conflicting deps (torch pins, numpy, python versions). Never share an env. Homebrew Python 3.14 is too new for ML wheels — per-tool venvs pin 3.10–3.12 |
|
||||
| DB | SQLite (WAL) | assets, jobs, workflow runs, provenance. Zero ops |
|
||||
| Job model | DAG of operator invocations, asyncio worker pool, one GPU-heavy job at a time (Metal), N CPU jobs parallel | 256GB lets several stay resident |
|
||||
| Frontend | React + Vite + Tailwind + three.js | drag/drop/paste ingest, GLB/splat preview, param forms |
|
||||
| Param forms | **auto-generated from operator JSON-Schema manifests** | "FULL CONTROL" without hand-building UI per tool — every flag a tool exposes becomes a form field with sane defaults |
|
||||
| Conversion | **Blender headless** (`blender -b -P convert.py`) as universal GLB↔FBX↔OBJ↔USD↔BLEND node + trimesh for light ops | FBX especially needs Blender |
|
||||
| Storage | `data/{inbox,assets,jobs,outputs}` content-addressed; archive operator → **M4 Pro over Tailscale (100.69.21.128, 4TB)** via rsync | hot on local/external SSD, cold on M4 Pro |
|
||||
| Access | bind 0.0.0.0, reach via Tailscale from any device | drive the beast from phone/laptop |
|
||||
| API keys | `.env` + settings page (fal, Tripo, Meshy, Replicate) | cloud operators light up when keys exist |
|
||||
|
||||
### The operator concept (the heart of "full control")
|
||||
Every capability — local or cloud — is one **operator**: a folder with `manifest.json` (name, input types, output types, JSON-Schema params, venv spec, resource class) and a `run.py`/shell entry. The UI reads manifests to render workflow choices and parameter forms; the DAG runner chains them by matching input/output types. Adding a new model = dropping in a new operator folder. Presets (saved DAGs + params) give one-click workflows:
|
||||
|
||||
- **"Video → Rigged Character"**: ffmpeg best-frame → trellis-mac (or Tripo API) → Mesh2Motion/Tripo-rig → retarget mocap → FBX
|
||||
- **"Video → Splat"**: ffmpeg → COLMAP/GLOMAP → Brush → .ply/.spz
|
||||
- **"Video → Scanned Mesh"**: ffmpeg → Object Capture → GLB/USDZ
|
||||
- **"Video → Mocap"**: FreeMoCap (or UE MetaHuman externally) → BVH/FBX
|
||||
- **"Image → Mesh ×N"**: fan the same image across trellis-mac + SF3D + Hunyuan + fal APIs, compare side-by-side in the viewer, keep the winner ← *the 256GB flex: run several models resident simultaneously*
|
||||
|
||||
### Format I/O matrix (via Blender headless + native tool outputs)
|
||||
In: mp4/mov, png/jpg/webp/heic, glb/gltf, obj, fbx, blend, usd(z), ply/spz.
|
||||
Out: glb, fbx, obj, usd(z), blend, ply/spz, bvh, abc, png sequences/turntable mp4 renders.
|
||||
|
||||
---
|
||||
|
||||
## 3. Build roadmap
|
||||
|
||||
- **Phase 0 — Skeleton (now)**: FastAPI + SQLite + asset ingest (upload/paste) + job queue + React UI with asset library, three.js GLB viewer, job dashboard. Operators: `ffprobe`, `ffmpeg-frames`, `blender-convert`. *Proves the whole loop: drop file → run op → preview → export.*
|
||||
- **Phase 1 — First real generators**: `sf3d` (fast local), `fal-trellis` + `fal-trellis2` (API, trivial), `trellis-mac` (the local flagship — expect real install debugging). Side-by-side compare view.
|
||||
- **Phase 2 — Scan track**: COLMAP+GLOMAP operator, Brush operator, Object Capture Swift CLI operator. Splat viewer in UI.
|
||||
- **Phase 3 — Motion track**: FreeMoCap operator, UE MetaHuman handoff (watch folder → ingest FBX back), GVHMR cloud-solve helper, Blender retarget operator (Rokoko/Retarget).
|
||||
- **Phase 4 — Character factory**: Tripo/Meshy full API operators (rig+animate), Mesh2Motion embed/bridge, Hunyuan3D-Omni posed-gen, LLM copilot pane (Ollama), archive-to-M4Pro operator, batch/watch-folder mode.
|
||||
|
||||
## 4. Open items
|
||||
- ~~UE MetaHuman markerless Mac verdict~~ **RESOLVED: Windows-only** (see Stage 4). Experimental plugin — Epic's pattern (Creator→Mac in 5.7, Animator facial→Mac in 5.8) suggests body could follow in 5.9+, but no commitment exists. Watch the Fab listing. Filming workflow for now: film 60fps HD/4K on tripod, single performer, form-fitting clothing; face-solve on Mac, body-solve on rented Windows GPU VM (batch design = pay only for solve hours).
|
||||
- M3 Ultra benchmarks for trellis-mac don't exist publicly yet (only M4 Pro ~3.5–5 min) — we'll be among the first; measure and record in this doc.
|
||||
- Disk: ~400GB free locally; weights alone (Trellis 15GB, Hunyuan ~50GB, Qwen 142GB…) justify the planned external SSD; M4 Pro is the archive tier.
|
||||
|
||||
## 5. Key sources
|
||||
trellis-mac github.com/shivampkumar/trellis-mac · TRELLIS.2 github.com/microsoft/TRELLIS.2 · SF3D github.com/Stability-AI/stable-fast-3d · Hunyuan3D 2.1 github.com/tencent-hunyuan/hunyuan3d-2.1 · Tripo pricing docs.tripo3d.ai/get-started/pricing.html · fal 3D fal.ai/3d-models · Brush github.com/ArthurBrussee/brush · OpenSplat github.com/pierotofy/OpenSplat · msplat github.com/rayanht/msplat · Object Capture developer.apple.com/documentation/RealityKit/creating-a-photogrammetry-command-line-app · FreeMoCap github.com/freemocap/freemocap · GVHMR github.com/zju3dv/GVHMR · Mesh2Motion github.com/Mesh2Motion/mesh2motion-app · UniRig github.com/VAST-AI-Research/UniRig · CEB_4d_Humans github.com/carlosedubarreto/CEB_4d_Humans · Meshcapade SMPL addon github.com/Meshcapade/SMPL_blender_addon
|
||||
32
README.md
Normal file
32
README.md
Normal file
@ -0,0 +1,32 @@
|
||||
# MODELBEAST
|
||||
|
||||
Local-first web app that turns videos, images, and 3D files into meshes, splats, mocap, and rigged characters on the M3 Ultra. See [PLAN.md](PLAN.md) for the full verified tool matrix and roadmap, and [HANDOFF.md](HANDOFF.md) for the agent build brief (phases 1–4 instructions).
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
/opt/homebrew/bin/uv run uvicorn server.main:app --host 0.0.0.0 --port 8777
|
||||
```
|
||||
|
||||
Open http://localhost:8777 (or `http://<tailscale-ip>:8777` from any device on the tailnet).
|
||||
|
||||
## Develop
|
||||
|
||||
- Backend: `server/` — FastAPI + SQLite (`data/modelbeast.db`), job runner executes operators as subprocesses.
|
||||
- Frontend: `web/` — React + Vite + three.js. After editing: `cd web && npm run build` (the server serves `web/dist`).
|
||||
- Data: `data/assets/` (asset store), `data/jobs/` (job workdirs). Delete `data/` to reset everything.
|
||||
|
||||
## Operators
|
||||
|
||||
Each subfolder of `server/operators/` with a `manifest.json` is an operator. The UI auto-renders its parameter form from `params_schema` (JSON Schema) and filters by the selected asset's kind (`accepts`).
|
||||
|
||||
Contract: the runner invokes `<python> run.py --input <asset> --outdir <jobdir> --params '<json>'`. Write outputs into the outdir; optionally write `result.json` (`{"outputs": [{"path": ..., "name": ..., "meta": ...}]}`) to control what gets registered as assets. stdout/stderr become the job log.
|
||||
|
||||
Current operators:
|
||||
| id | what |
|
||||
|---|---|
|
||||
| `ffprobe` | media inspection |
|
||||
| `ffmpeg_frames` | video → frames (fps, mpdecimate dedupe, blur cull, max cap) |
|
||||
| `blender_convert` | GLB/GLTF/OBJ/FBX/STL/PLY/USD/BLEND → GLB/FBX/OBJ/USD/USDZ/STL/PLY/BLEND via headless Blender |
|
||||
|
||||
Heavy operators (trellis-mac, SF3D, COLMAP/Brush, FreeMoCap…) get their own uv venv: set `"python": "<venv python path>"` in the manifest. See PLAN.md Phase 1–4.
|
||||
10
pyproject.toml
Normal file
10
pyproject.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[project]
|
||||
name = "modelbeast"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"aiofiles>=25.1.0",
|
||||
"fastapi>=0.139.0",
|
||||
"python-multipart>=0.0.32",
|
||||
"uvicorn[standard]>=0.51.0",
|
||||
]
|
||||
0
server/__init__.py
Normal file
0
server/__init__.py
Normal file
65
server/db.py
Normal file
65
server/db.py
Normal file
@ -0,0 +1,65 @@
|
||||
import json
|
||||
import sqlite3
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DATA = ROOT / "data"
|
||||
DB_PATH = DATA / "modelbeast.db"
|
||||
|
||||
SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS assets (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
size INTEGER NOT NULL DEFAULT 0,
|
||||
meta TEXT NOT NULL DEFAULT '{}',
|
||||
parent_job TEXT,
|
||||
created_at REAL NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
operator TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'queued',
|
||||
asset_id TEXT,
|
||||
params TEXT NOT NULL DEFAULT '{}',
|
||||
outdir TEXT,
|
||||
log TEXT NOT NULL DEFAULT '',
|
||||
error TEXT,
|
||||
created_at REAL NOT NULL,
|
||||
started_at REAL,
|
||||
finished_at REAL
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
def connect() -> sqlite3.Connection:
|
||||
DATA.mkdir(parents=True, exist_ok=True)
|
||||
# FastAPI sync endpoints run in a threadpool; python sqlite3 is built in
|
||||
# serialized threading mode, so sharing one connection across threads is safe.
|
||||
con = sqlite3.connect(DB_PATH, check_same_thread=False)
|
||||
con.row_factory = sqlite3.Row
|
||||
con.execute("PRAGMA journal_mode=WAL")
|
||||
con.executescript(SCHEMA)
|
||||
return con
|
||||
|
||||
|
||||
def new_id() -> str:
|
||||
return uuid.uuid4().hex[:12]
|
||||
|
||||
|
||||
def now() -> float:
|
||||
return time.time()
|
||||
|
||||
|
||||
def row_to_dict(row: sqlite3.Row) -> dict:
|
||||
d = dict(row)
|
||||
for key in ("meta", "params"):
|
||||
if key in d and isinstance(d[key], str):
|
||||
try:
|
||||
d[key] = json.loads(d[key])
|
||||
except ValueError:
|
||||
pass
|
||||
return d
|
||||
113
server/main.py
Normal file
113
server/main.py
Normal file
@ -0,0 +1,113 @@
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, File, HTTPException, UploadFile, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from . import db, registry, store
|
||||
from .runner import runner
|
||||
|
||||
app = FastAPI(title="MODELBEAST")
|
||||
WEB_DIST = db.ROOT / "web" / "dist"
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
runner.operators = registry.load_operators()
|
||||
app.state.con = db.connect()
|
||||
asyncio.create_task(runner.worker())
|
||||
|
||||
|
||||
# -- operators ----------------------------------------------------------------
|
||||
@app.get("/api/operators")
|
||||
def list_operators():
|
||||
ops = []
|
||||
for op in runner.operators.values():
|
||||
public = {k: v for k, v in op.items() if k != "dir"}
|
||||
ops.append(public)
|
||||
return ops
|
||||
|
||||
|
||||
# -- assets ---------------------------------------------------------------------
|
||||
@app.get("/api/assets")
|
||||
def list_assets():
|
||||
return store.list_assets(app.state.con)
|
||||
|
||||
|
||||
@app.post("/api/assets")
|
||||
async def upload_asset(file: UploadFile = File(...)):
|
||||
data = await file.read()
|
||||
if not data:
|
||||
raise HTTPException(400, "empty file")
|
||||
return store.register_upload(app.state.con, file.filename or "unnamed", data)
|
||||
|
||||
|
||||
@app.delete("/api/assets/{asset_id}")
|
||||
def delete_asset(asset_id: str):
|
||||
if not store.delete_asset(app.state.con, asset_id):
|
||||
raise HTTPException(404, "no such asset")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.get("/api/assets/{asset_id}/file")
|
||||
def asset_file(asset_id: str, member: str | None = None):
|
||||
asset = store.get_asset(app.state.con, asset_id)
|
||||
if not asset:
|
||||
raise HTTPException(404, "no such asset")
|
||||
path = Path(asset["path"])
|
||||
if member: # file inside a folder asset (e.g. one frame)
|
||||
target = (path / member).resolve()
|
||||
if not str(target).startswith(str(path.resolve())) or not target.is_file():
|
||||
raise HTTPException(404, "no such member")
|
||||
return FileResponse(target)
|
||||
if path.is_dir():
|
||||
files = sorted(p.name for p in path.iterdir() if p.is_file())
|
||||
return JSONResponse({"folder": True, "files": files})
|
||||
return FileResponse(path, filename=asset["name"])
|
||||
|
||||
|
||||
# -- jobs -----------------------------------------------------------------------
|
||||
@app.get("/api/jobs")
|
||||
def list_jobs():
|
||||
return runner.list_jobs(app.state.con)
|
||||
|
||||
|
||||
@app.post("/api/jobs")
|
||||
async def create_job(payload: dict):
|
||||
operator = payload.get("operator")
|
||||
if operator not in runner.operators:
|
||||
raise HTTPException(400, f"unknown operator: {operator}")
|
||||
asset_id = payload.get("asset_id")
|
||||
if asset_id and not store.get_asset(app.state.con, asset_id):
|
||||
raise HTTPException(400, "unknown asset")
|
||||
params = payload.get("params") or {}
|
||||
job = runner.create_job(app.state.con, operator, asset_id, params)
|
||||
await runner.broadcast({"type": "job", "job": job})
|
||||
return job
|
||||
|
||||
|
||||
@app.get("/api/jobs/{job_id}")
|
||||
def get_job(job_id: str):
|
||||
job = runner.get_job(app.state.con, job_id)
|
||||
if not job:
|
||||
raise HTTPException(404, "no such job")
|
||||
return job
|
||||
|
||||
|
||||
# -- websocket ---------------------------------------------------------------
|
||||
@app.websocket("/ws")
|
||||
async def ws(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
runner.subscribers.add(websocket)
|
||||
try:
|
||||
while True:
|
||||
await websocket.receive_text() # keepalive pings from client
|
||||
except WebSocketDisconnect:
|
||||
runner.subscribers.discard(websocket)
|
||||
|
||||
|
||||
# -- frontend (built) -----------------------------------------------------------
|
||||
if WEB_DIST.exists():
|
||||
app.mount("/", StaticFiles(directory=WEB_DIST, html=True), name="web")
|
||||
19
server/operators/blender_convert/manifest.json
Normal file
19
server/operators/blender_convert/manifest.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"id": "blender_convert",
|
||||
"name": "Convert 3D Format",
|
||||
"category": "convert",
|
||||
"description": "Universal 3D format conversion via headless Blender: GLB/GLTF/OBJ/FBX/USD/STL/PLY/BLEND in, GLB/FBX/OBJ/USD/STL/PLY/BLEND out.",
|
||||
"accepts": ["model"],
|
||||
"produces": ["model"],
|
||||
"entry": "run.py",
|
||||
"params_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": {"type": "string",
|
||||
"enum": ["glb", "fbx", "obj", "usd", "usdz", "stl", "ply", "blend"],
|
||||
"default": "glb", "description": "Output format"},
|
||||
"apply_transforms": {"type": "boolean", "default": true},
|
||||
"scale": {"type": "number", "default": 1.0, "minimum": 0.001, "maximum": 1000}
|
||||
}
|
||||
}
|
||||
}
|
||||
83
server/operators/blender_convert/run.py
Normal file
83
server/operators/blender_convert/run.py
Normal file
@ -0,0 +1,83 @@
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
BLENDER = "/Applications/Blender.app/Contents/MacOS/Blender"
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--input", required=True)
|
||||
ap.add_argument("--outdir", required=True)
|
||||
ap.add_argument("--params", default="{}")
|
||||
args = ap.parse_args()
|
||||
p = json.loads(args.params)
|
||||
|
||||
target = p.get("target", "glb")
|
||||
outdir = Path(args.outdir)
|
||||
out_name = Path(args.input).stem + "." + target
|
||||
out_path = outdir / out_name
|
||||
|
||||
bpy_script = outdir / "_convert.py"
|
||||
bpy_script.write_text(f"""
|
||||
import bpy, json, sys
|
||||
src = {json.dumps(args.input)}
|
||||
dst = {json.dumps(str(out_path))}
|
||||
params = json.loads({json.dumps(json.dumps(p))})
|
||||
ext = src.rsplit('.', 1)[-1].lower()
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
importers = {{
|
||||
'glb': lambda: bpy.ops.import_scene.gltf(filepath=src),
|
||||
'gltf': lambda: bpy.ops.import_scene.gltf(filepath=src),
|
||||
'obj': lambda: bpy.ops.wm.obj_import(filepath=src),
|
||||
'fbx': lambda: bpy.ops.import_scene.fbx(filepath=src),
|
||||
'stl': lambda: bpy.ops.wm.stl_import(filepath=src),
|
||||
'ply': lambda: bpy.ops.wm.ply_import(filepath=src),
|
||||
'usd': lambda: bpy.ops.wm.usd_import(filepath=src),
|
||||
'usdz': lambda: bpy.ops.wm.usd_import(filepath=src),
|
||||
'usda': lambda: bpy.ops.wm.usd_import(filepath=src),
|
||||
'dae': lambda: bpy.ops.wm.collada_import(filepath=src),
|
||||
'abc': lambda: bpy.ops.wm.alembic_import(filepath=src),
|
||||
}}
|
||||
if ext == 'blend':
|
||||
bpy.ops.wm.open_mainfile(filepath=src)
|
||||
else:
|
||||
importers[ext]()
|
||||
|
||||
scale = params.get('scale', 1.0)
|
||||
if scale != 1.0:
|
||||
for ob in bpy.context.scene.objects:
|
||||
ob.scale = [s * scale for s in ob.scale]
|
||||
if params.get('apply_transforms', True):
|
||||
for ob in bpy.context.scene.objects:
|
||||
ob.select_set(True)
|
||||
if bpy.context.scene.objects:
|
||||
bpy.context.view_layer.objects.active = bpy.context.scene.objects[0]
|
||||
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
|
||||
|
||||
tgt = dst.rsplit('.', 1)[-1].lower()
|
||||
exporters = {{
|
||||
'glb': lambda: bpy.ops.export_scene.gltf(filepath=dst, export_format='GLB'),
|
||||
'fbx': lambda: bpy.ops.export_scene.fbx(filepath=dst),
|
||||
'obj': lambda: bpy.ops.wm.obj_export(filepath=dst),
|
||||
'stl': lambda: bpy.ops.wm.stl_export(filepath=dst),
|
||||
'ply': lambda: bpy.ops.wm.ply_export(filepath=dst),
|
||||
'usd': lambda: bpy.ops.wm.usd_export(filepath=dst),
|
||||
'usdz': lambda: bpy.ops.wm.usd_export(filepath=dst),
|
||||
'blend': lambda: bpy.ops.wm.save_as_mainfile(filepath=dst),
|
||||
}}
|
||||
exporters[tgt]()
|
||||
print('exported', dst)
|
||||
""")
|
||||
|
||||
cmd = [BLENDER, "-b", "--python", str(bpy_script)]
|
||||
print("+", " ".join(cmd), flush=True)
|
||||
res = subprocess.run(cmd, stdout=sys.stdout, stderr=subprocess.STDOUT)
|
||||
bpy_script.unlink(missing_ok=True)
|
||||
if res.returncode != 0 or not out_path.exists():
|
||||
sys.exit(res.returncode or 1)
|
||||
|
||||
(outdir / "result.json").write_text(json.dumps(
|
||||
{"outputs": [{"path": out_name}]}))
|
||||
print("done", flush=True)
|
||||
23
server/operators/ffmpeg_frames/manifest.json
Normal file
23
server/operators/ffmpeg_frames/manifest.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"id": "ffmpeg_frames",
|
||||
"name": "Extract Frames",
|
||||
"category": "ingest",
|
||||
"description": "Extract frames from video for photogrammetry/3DGS/mesh-gen. Supports fps sampling, near-duplicate removal (mpdecimate), and sharpness culling.",
|
||||
"accepts": ["video"],
|
||||
"produces": ["frames"],
|
||||
"entry": "run.py",
|
||||
"params_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fps": {"type": "number", "default": 2, "minimum": 0.1, "maximum": 60,
|
||||
"description": "Frames per second to sample (2-3 for orbit scans)"},
|
||||
"dedupe": {"type": "boolean", "default": true,
|
||||
"description": "Drop near-duplicate frames (mpdecimate)"},
|
||||
"format": {"type": "string", "enum": ["jpg", "png"], "default": "jpg"},
|
||||
"max_frames": {"type": "integer", "default": 300, "minimum": 1, "maximum": 5000,
|
||||
"description": "Hard cap on frames kept"},
|
||||
"sharpness_cull_pct": {"type": "number", "default": 5, "minimum": 0, "maximum": 50,
|
||||
"description": "Cull the blurriest N% (by file size heuristic, jpg only)"}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
server/operators/ffmpeg_frames/run.py
Normal file
59
server/operators/ffmpeg_frames/run.py
Normal file
@ -0,0 +1,59 @@
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--input", required=True)
|
||||
ap.add_argument("--outdir", required=True)
|
||||
ap.add_argument("--params", default="{}")
|
||||
args = ap.parse_args()
|
||||
p = json.loads(args.params)
|
||||
|
||||
fps = p.get("fps", 2)
|
||||
dedupe = p.get("dedupe", True)
|
||||
fmt = p.get("format", "jpg")
|
||||
max_frames = int(p.get("max_frames", 300))
|
||||
cull_pct = float(p.get("sharpness_cull_pct", 5))
|
||||
|
||||
outdir = Path(args.outdir)
|
||||
stem = Path(args.input).stem
|
||||
frames_dir = outdir / f"{stem}_frames"
|
||||
frames_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
vf = f"fps={fps}"
|
||||
if dedupe:
|
||||
vf = f"mpdecimate,{vf}"
|
||||
cmd = ["ffmpeg", "-y", "-i", args.input, "-vf", vf, "-vsync", "vfr"]
|
||||
if fmt == "jpg":
|
||||
cmd += ["-q:v", "1"]
|
||||
cmd += [str(frames_dir / f"%05d.{fmt}")]
|
||||
print("+", " ".join(cmd), flush=True)
|
||||
subprocess.run(cmd, check=True, stderr=sys.stdout)
|
||||
|
||||
frames = sorted(frames_dir.glob(f"*.{fmt}"))
|
||||
print(f"extracted {len(frames)} frames", flush=True)
|
||||
|
||||
# blur cull: motion-blurred jpgs compress smaller — drop the smallest N%
|
||||
if fmt == "jpg" and cull_pct > 0 and len(frames) > 20:
|
||||
by_size = sorted(frames, key=lambda f: f.stat().st_size)
|
||||
n_cull = int(len(frames) * cull_pct / 100)
|
||||
for f in by_size[:n_cull]:
|
||||
f.unlink()
|
||||
print(f"culled {n_cull} likely-blurry frames", flush=True)
|
||||
frames = sorted(frames_dir.glob(f"*.{fmt}"))
|
||||
|
||||
if len(frames) > max_frames:
|
||||
keep_every = len(frames) / max_frames
|
||||
keep = {frames[int(i * keep_every)] for i in range(max_frames)}
|
||||
for f in frames:
|
||||
if f not in keep:
|
||||
f.unlink()
|
||||
frames = sorted(frames_dir.glob(f"*.{fmt}"))
|
||||
print(f"thinned to {len(frames)} frames (max_frames={max_frames})", flush=True)
|
||||
|
||||
(outdir / "result.json").write_text(json.dumps({
|
||||
"outputs": [{"path": frames_dir.name,
|
||||
"meta": {"count": len(frames), "fps": fps, "format": fmt}}]}))
|
||||
print("done", flush=True)
|
||||
13
server/operators/ffprobe/manifest.json
Normal file
13
server/operators/ffprobe/manifest.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "ffprobe",
|
||||
"name": "Inspect Media",
|
||||
"category": "ingest",
|
||||
"description": "Probe a video/image/audio file: codec, resolution, duration, fps, streams.",
|
||||
"accepts": ["video", "image"],
|
||||
"produces": ["report"],
|
||||
"entry": "run.py",
|
||||
"params_schema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
36
server/operators/ffprobe/run.py
Normal file
36
server/operators/ffprobe/run.py
Normal file
@ -0,0 +1,36 @@
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--input", required=True)
|
||||
ap.add_argument("--outdir", required=True)
|
||||
ap.add_argument("--params", default="{}")
|
||||
args = ap.parse_args()
|
||||
|
||||
out = subprocess.run(
|
||||
["ffprobe", "-v", "error", "-print_format", "json",
|
||||
"-show_format", "-show_streams", args.input],
|
||||
capture_output=True, text=True, check=True)
|
||||
probe = json.loads(out.stdout)
|
||||
|
||||
summary = {"streams": []}
|
||||
fmt = probe.get("format", {})
|
||||
summary["container"] = fmt.get("format_long_name")
|
||||
summary["duration_s"] = float(fmt.get("duration", 0) or 0)
|
||||
summary["size_bytes"] = int(fmt.get("size", 0) or 0)
|
||||
for s in probe.get("streams", []):
|
||||
entry = {"type": s.get("codec_type"), "codec": s.get("codec_name")}
|
||||
if s.get("codec_type") == "video":
|
||||
entry["resolution"] = f"{s.get('width')}x{s.get('height')}"
|
||||
entry["fps"] = s.get("avg_frame_rate")
|
||||
entry["frames"] = s.get("nb_frames")
|
||||
summary["streams"].append(entry)
|
||||
print(json.dumps(summary, indent=2))
|
||||
|
||||
outdir = Path(args.outdir)
|
||||
name = Path(args.input).stem + ".probe.json"
|
||||
(outdir / name).write_text(json.dumps({"summary": summary, "raw": probe}, indent=2))
|
||||
(outdir / "result.json").write_text(json.dumps(
|
||||
{"outputs": [{"path": name, "meta": {"summary": summary}}]}))
|
||||
18
server/registry.py
Normal file
18
server/registry.py
Normal file
@ -0,0 +1,18 @@
|
||||
"""Operator registry: each subfolder of server/operators with a manifest.json is an operator."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
OPERATORS_DIR = Path(__file__).resolve().parent / "operators"
|
||||
|
||||
|
||||
def load_operators() -> dict[str, dict]:
|
||||
ops = {}
|
||||
for manifest_path in sorted(OPERATORS_DIR.glob("*/manifest.json")):
|
||||
try:
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
except ValueError as e:
|
||||
print(f"[registry] bad manifest {manifest_path}: {e}")
|
||||
continue
|
||||
manifest["dir"] = str(manifest_path.parent)
|
||||
ops[manifest["id"]] = manifest
|
||||
return ops
|
||||
143
server/runner.py
Normal file
143
server/runner.py
Normal file
@ -0,0 +1,143 @@
|
||||
"""Job runner: executes operators as subprocesses, one heavy job at a time.
|
||||
|
||||
Operator contract:
|
||||
<python> run.py --input <asset path> --outdir <job outdir> --params '<json>'
|
||||
- stdout/stderr are streamed into the job log
|
||||
- operator writes outputs into outdir and (optionally) outdir/result.json:
|
||||
{"outputs": [{"path": "relative/or/absolute", "name": "...", "meta": {...}}, ...],
|
||||
"summary": {...}}
|
||||
- if result.json is absent, every top-level file in outdir is registered as an asset
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from . import db, store
|
||||
|
||||
JOBS_DIR = db.DATA / "jobs"
|
||||
|
||||
|
||||
class Runner:
|
||||
def __init__(self):
|
||||
self.queue: asyncio.Queue[str] = asyncio.Queue()
|
||||
self.subscribers: set = set() # websockets
|
||||
self.operators: dict[str, dict] = {}
|
||||
|
||||
# -- pubsub -------------------------------------------------------------
|
||||
async def broadcast(self, message: dict):
|
||||
dead = []
|
||||
for ws in self.subscribers:
|
||||
try:
|
||||
await ws.send_json(message)
|
||||
except Exception:
|
||||
dead.append(ws)
|
||||
for ws in dead:
|
||||
self.subscribers.discard(ws)
|
||||
|
||||
# -- job lifecycle --------------------------------------------------------
|
||||
def create_job(self, con, operator: str, asset_id: str | None, params: dict) -> dict:
|
||||
job_id = db.new_id()
|
||||
outdir = JOBS_DIR / job_id
|
||||
outdir.mkdir(parents=True, exist_ok=True)
|
||||
con.execute(
|
||||
"INSERT INTO jobs (id, operator, status, asset_id, params, outdir, created_at) "
|
||||
"VALUES (?, ?, 'queued', ?, ?, ?, ?)",
|
||||
(job_id, operator, asset_id, json.dumps(params), str(outdir), db.now()),
|
||||
)
|
||||
con.commit()
|
||||
self.queue.put_nowait(job_id)
|
||||
return self.get_job(con, job_id)
|
||||
|
||||
def get_job(self, con, job_id: str) -> dict | None:
|
||||
row = con.execute("SELECT * FROM jobs WHERE id = ?", (job_id,)).fetchone()
|
||||
return db.row_to_dict(row) if row else None
|
||||
|
||||
def list_jobs(self, con) -> list[dict]:
|
||||
rows = con.execute("SELECT * FROM jobs ORDER BY created_at DESC LIMIT 200").fetchall()
|
||||
return [db.row_to_dict(r) for r in rows]
|
||||
|
||||
async def _update(self, con, job_id: str, **fields):
|
||||
sets = ", ".join(f"{k} = ?" for k in fields)
|
||||
con.execute(f"UPDATE jobs SET {sets} WHERE id = ?", (*fields.values(), job_id))
|
||||
con.commit()
|
||||
job = self.get_job(con, job_id)
|
||||
await self.broadcast({"type": "job", "job": job})
|
||||
|
||||
# -- worker -----------------------------------------------------------------
|
||||
async def worker(self):
|
||||
con = db.connect()
|
||||
# re-queue jobs that were left running/queued by a previous process
|
||||
for row in con.execute("SELECT id FROM jobs WHERE status IN ('queued','running')"):
|
||||
con.execute("UPDATE jobs SET status='queued' WHERE id = ?", (row["id"],))
|
||||
self.queue.put_nowait(row["id"])
|
||||
con.commit()
|
||||
while True:
|
||||
job_id = await self.queue.get()
|
||||
try:
|
||||
await self._run_job(con, job_id)
|
||||
except Exception as e:
|
||||
await self._update(con, job_id, status="error", error=str(e), finished_at=db.now())
|
||||
|
||||
async def _run_job(self, con, job_id: str):
|
||||
job = self.get_job(con, job_id)
|
||||
if not job:
|
||||
return
|
||||
op = self.operators.get(job["operator"])
|
||||
if not op:
|
||||
await self._update(con, job_id, status="error", error=f"unknown operator {job['operator']}",
|
||||
finished_at=db.now())
|
||||
return
|
||||
asset = store.get_asset(con, job["asset_id"]) if job["asset_id"] else None
|
||||
outdir = Path(job["outdir"])
|
||||
entry = Path(op["dir"]) / op.get("entry", "run.py")
|
||||
python = op.get("python") or sys.executable
|
||||
cmd = [python, str(entry), "--outdir", str(outdir),
|
||||
"--params", json.dumps(job["params"])]
|
||||
if asset:
|
||||
cmd += ["--input", asset["path"]]
|
||||
|
||||
await self._update(con, job_id, status="running", started_at=db.now())
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT)
|
||||
log_lines: list[str] = []
|
||||
assert proc.stdout
|
||||
async for raw in proc.stdout:
|
||||
line = raw.decode(errors="replace")
|
||||
log_lines.append(line)
|
||||
if len(log_lines) % 5 == 0: # don't hammer the DB on chatty tools
|
||||
await self._update(con, job_id, log="".join(log_lines)[-100_000:])
|
||||
code = await proc.wait()
|
||||
log = "".join(log_lines)[-100_000:]
|
||||
|
||||
if code != 0:
|
||||
await self._update(con, job_id, status="error", log=log,
|
||||
error=f"exit code {code}", finished_at=db.now())
|
||||
return
|
||||
|
||||
# register outputs
|
||||
result_path = outdir / "result.json"
|
||||
registered = []
|
||||
if result_path.exists():
|
||||
result = json.loads(result_path.read_text())
|
||||
for out in result.get("outputs", []):
|
||||
p = Path(out["path"])
|
||||
if not p.is_absolute():
|
||||
p = outdir / p
|
||||
if p.exists():
|
||||
a = store.register_file(con, p, name=out.get("name"),
|
||||
parent_job=job_id, move=True,
|
||||
meta=out.get("meta"))
|
||||
registered.append(a["id"])
|
||||
else:
|
||||
for p in sorted(outdir.iterdir()):
|
||||
if p.name == "result.json" or p.name.startswith("."):
|
||||
continue
|
||||
a = store.register_file(con, p, parent_job=job_id, move=True)
|
||||
registered.append(a["id"])
|
||||
|
||||
await self._update(con, job_id, status="done", log=log, finished_at=db.now())
|
||||
await self.broadcast({"type": "assets_changed", "job_id": job_id, "assets": registered})
|
||||
|
||||
|
||||
runner = Runner()
|
||||
89
server/store.py
Normal file
89
server/store.py
Normal file
@ -0,0 +1,89 @@
|
||||
"""Asset storage: files land in data/assets/<id>/<name>, kinds inferred by extension."""
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from . import db
|
||||
|
||||
ASSETS_DIR = db.DATA / "assets"
|
||||
|
||||
KIND_BY_EXT = {
|
||||
"video": {".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v"},
|
||||
"image": {".png", ".jpg", ".jpeg", ".webp", ".heic", ".bmp", ".tif", ".tiff"},
|
||||
"model": {".glb", ".gltf", ".obj", ".fbx", ".blend", ".usd", ".usdz", ".usda",
|
||||
".ply", ".stl", ".spz", ".abc", ".dae", ".bvh"},
|
||||
}
|
||||
|
||||
|
||||
def kind_of(name: str) -> str:
|
||||
ext = Path(name).suffix.lower()
|
||||
for kind, exts in KIND_BY_EXT.items():
|
||||
if ext in exts:
|
||||
return kind
|
||||
return "other"
|
||||
|
||||
|
||||
def register_file(con, src: Path, name: str | None = None, parent_job: str | None = None,
|
||||
move: bool = False, meta: dict | None = None) -> dict:
|
||||
"""Copy/move a file (or directory) into the asset store and record it."""
|
||||
name = name or src.name
|
||||
asset_id = db.new_id()
|
||||
dest_dir = ASSETS_DIR / asset_id
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
dest = dest_dir / name
|
||||
if src.is_dir():
|
||||
if move:
|
||||
shutil.move(str(src), dest)
|
||||
else:
|
||||
shutil.copytree(src, dest)
|
||||
size = sum(f.stat().st_size for f in dest.rglob("*") if f.is_file())
|
||||
kind = "frames" if any(dest.glob("*.jpg")) or any(dest.glob("*.png")) else "folder"
|
||||
else:
|
||||
if move:
|
||||
shutil.move(str(src), dest)
|
||||
else:
|
||||
shutil.copy2(src, dest)
|
||||
size = dest.stat().st_size
|
||||
kind = kind_of(name)
|
||||
con.execute(
|
||||
"INSERT INTO assets (id, name, kind, path, size, meta, parent_job, created_at) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(asset_id, name, kind, str(dest), size, json.dumps(meta or {}), parent_job, db.now()),
|
||||
)
|
||||
con.commit()
|
||||
return get_asset(con, asset_id)
|
||||
|
||||
|
||||
def register_upload(con, name: str, data: bytes) -> dict:
|
||||
asset_id = db.new_id()
|
||||
dest_dir = ASSETS_DIR / asset_id
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
dest = dest_dir / name
|
||||
dest.write_bytes(data)
|
||||
con.execute(
|
||||
"INSERT INTO assets (id, name, kind, path, size, meta, parent_job, created_at) "
|
||||
"VALUES (?, ?, ?, ?, ?, '{}', NULL, ?)",
|
||||
(asset_id, name, kind_of(name), str(dest), len(data), db.now()),
|
||||
)
|
||||
con.commit()
|
||||
return get_asset(con, asset_id)
|
||||
|
||||
|
||||
def get_asset(con, asset_id: str) -> dict | None:
|
||||
row = con.execute("SELECT * FROM assets WHERE id = ?", (asset_id,)).fetchone()
|
||||
return db.row_to_dict(row) if row else None
|
||||
|
||||
|
||||
def list_assets(con) -> list[dict]:
|
||||
rows = con.execute("SELECT * FROM assets ORDER BY created_at DESC").fetchall()
|
||||
return [db.row_to_dict(r) for r in rows]
|
||||
|
||||
|
||||
def delete_asset(con, asset_id: str) -> bool:
|
||||
asset = get_asset(con, asset_id)
|
||||
if not asset:
|
||||
return False
|
||||
shutil.rmtree(ASSETS_DIR / asset_id, ignore_errors=True)
|
||||
con.execute("DELETE FROM assets WHERE id = ?", (asset_id,))
|
||||
con.commit()
|
||||
return True
|
||||
559
uv.lock
generated
Normal file
559
uv.lock
generated
Normal file
@ -0,0 +1,559 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "aiofiles"
|
||||
version = "25.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-doc"
|
||||
version = "0.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.14.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" },
|
||||
]
|
||||
|
||||
[[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 = "fastapi"
|
||||
version = "0.139.0"
|
||||
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/d3/af/a5f50ccfa659ec1802cb4ca842c23f06d906a8cc9aef6016a2caeea3d4ed/fastapi-0.139.0.tar.gz", hash = "sha256:99ab7b2d92223c76d6cf10757ab3f89d45b38267fc20b2a136cf02f6beac3145", size = 423016, upload-time = "2026-07-01T16:35:33.436Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/7c/8e3c6ad324ea5cb36604fc3f968554887891c316d9dfde57761611d907ad/fastapi-0.139.0-py3-none-any.whl", hash = "sha256:cf15e1e9e667ddb0ad63811e60bd11390d1aac838ca4a7a23f421807b2308189", size = 130339, upload-time = "2026-07-01T16:35:32.19Z" },
|
||||
]
|
||||
|
||||
[[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 = "httptools"
|
||||
version = "0.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" },
|
||||
]
|
||||
|
||||
[[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 = "modelbeast"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "aiofiles" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiofiles", specifier = ">=25.1.0" },
|
||||
{ name = "fastapi", specifier = ">=0.139.0" },
|
||||
{ name = "python-multipart", specifier = ">=0.0.32" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.51.0" },
|
||||
]
|
||||
|
||||
[[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/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/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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.32"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" },
|
||||
]
|
||||
|
||||
[[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/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 = "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 = "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 = "uvicorn"
|
||||
version = "0.51.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/65/b7c6c443ccc58678c91e1e973bbe2a878591538655d6e1d47f24ba1c51f3/uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0", size = 94412, upload-time = "2026-07-08T10:59:05.962Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/45/ec/dbb7e5a6b91f86bfb9eb7d2988a2730907b6a729875b949c7f022e8b88fa/uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", size = 73219, upload-time = "2026-07-08T10:59:04.44Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
standard = [
|
||||
{ name = "httptools" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
|
||||
{ name = "watchfiles" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvloop"
|
||||
version = "0.22.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "watchfiles"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "16.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/02/b9a097e1e16fee4e2fd1ec8c39f6a9c5d6257bae8fa12640caf869f54436/websockets-16.1.tar.gz", hash = "sha256:299468cbe42e2b9981134c7c51d99387d8a7bf562b00183b3eec53f882846dad", size = 182530, upload-time = "2026-07-10T06:32:57.734Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/52/748c014f07f4e0e170c8932de7e647a1511d5ab3049cd978797136aee577/websockets-16.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b6aa3f7ad345cf3862c21f4fbf2ef5e14d911348476c2845e137c091fe3a3f0b", size = 179798, upload-time = "2026-07-10T06:31:09.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/5e/2a2e64d977d084e49d37c187c26c056daaff41965be7300cd5dbde6f8b07/websockets-16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b43fcfb521ac2f34ba80b7b8ea16303e4ad82dd8af667bf40839ad3a5d37b164", size = 177478, upload-time = "2026-07-10T06:31:11.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/12/5b85b4e75d697e548a94962ce5c036b05dd21cb9545759d555c5586422fc/websockets-16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2bd3e12cd9afbe2baedae0b1eeade8ba64329b60fe2f9abdc966bd10fd2c2ef5", size = 177746, upload-time = "2026-07-10T06:31:12.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/62/79b1c8f0cee0da648b4899e1c5b0dbd3aa59846985136a54854db6827ab4/websockets-16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f41979c8623df9bd30d949d82010a8fda5c56ff12cd8508a5b7272b6d4b53a", size = 187345, upload-time = "2026-07-10T06:31:13.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/34/b7c5c52c2f24280e1c017acb7ad491a566750a5cceca7f3cf999373bba21/websockets-16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a24d1f35aef07d794a16c853c688e74956c50239bec37b4f2de080056046419b", size = 188581, upload-time = "2026-07-10T06:31:15.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/37/604193bebcbeffe96fdf795960b83a15d600880c64dc17ec9c31c5b3427d/websockets-16.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c64c024ddf7a35331b21fcddb562a039c275d2c82e8c2d12939e7da23997270", size = 191362, upload-time = "2026-07-10T06:31:16.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/b4/5ee27575b367d7110d4d13945e2a9de067ec84dc71e54b87f01e38550d9a/websockets-16.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e99757f5baafe20fc598e202ea6f5b0b265186ad38d0a17bd8beca16296955", size = 189216, upload-time = "2026-07-10T06:31:17.776Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/22/3e2dcc78d85fc5d9d814895ce6d07d0dfacc0f6aaa1d151f2b8c8d772299/websockets-16.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:353f3bc6e058ac1ccab4b3588e8598837a8c04cfc8351233e6d523be675d844c", size = 187971, upload-time = "2026-07-10T06:31:19.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/2f/cd271717b93d5ee19626cb5e38a85baab745c86e33db7c31a3ac729b31b8/websockets-16.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0352f5b38b40e857b6428d468fa21dbb4dd4a567d933c26d9831b4efe1b92f43", size = 185381, upload-time = "2026-07-10T06:31:20.665Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/91/6ad6f2f1426317b5001bd490534208c7360636b35bac1dec2e0c22bfc40e/websockets-16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70bd789afab579602968c39f21cb925466505f3edff22f0ae852bca54978a4f9", size = 188015, upload-time = "2026-07-10T06:31:22.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/6d/533733132ab4c07540efd4a8f0b9a435d3a5059b2f26cc476ace1abf7f45/websockets-16.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d0fb4b46f121eccd539353baebd1083a8767a9a351109453d1d1caecd1ba40c2", size = 186619, upload-time = "2026-07-10T06:31:23.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/73/16c059f3d73b3331eba10793704afa4faa9939234fb08ef7dca35794e8f0/websockets-16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c14b6634af01541e4efe2954fd8f263386f7aa6d37c01e55dd8109fd17661452", size = 188497, upload-time = "2026-07-10T06:31:25.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/89/9a8fae7dd2acdcfb1a8844c29fe42b518a04b64fce38a0923b6290e452f1/websockets-16.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:a58532c49a851bcb481e58c1be23b315c17fe2fbbed509d75aeea12f543d2c15", size = 186051, upload-time = "2026-07-10T06:31:26.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/40/b240c7dd6a0e0c59c1f68377cc3015263521080c327c15f5e753c1f6d378/websockets-16.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4e969170c3b08e1d8dabd990fef1fa702c4233aeaabec33f871806e444f6a0e4", size = 187029, upload-time = "2026-07-10T06:31:27.605Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/35/524e3fac40e47d6fdcf6c4b2c95ef1bc8a97e01593c90eff86621df7b716/websockets-16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff9b000064b88787ba9f7a3cb2af2b68a658ca5aad76458a46469e7124b678a0", size = 187308, upload-time = "2026-07-10T06:31:28.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/13/56840cf62c8859af6ba22b9529da937332468c80f32b598753e8a66d3990/websockets-16.1-cp312-cp312-win32.whl", hash = "sha256:b9f5d83f80f4d7c4bba6d97f3755ac05850c784dce0fd2ab371c4e41172f53ff", size = 180161, upload-time = "2026-07-10T06:31:30.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/ff/87eb9eb44cb62424a8d729834f2b0515a47e2669fabec29820268f4d50a1/websockets-16.1-cp312-cp312-win_amd64.whl", hash = "sha256:6852c9f653966c16109d3b6f31181fd734f7914927e3f0fa1117af7a18c9aa21", size = 180462, upload-time = "2026-07-10T06:31:31.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/63/df158b155420b566f025e75613424ad9649a24bcb0e9f259321ab3d58bea/websockets-16.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b0232ed141cec3df2af5a3959a071c51f40036336b0d37e17faf9ef52fc73e47", size = 179791, upload-time = "2026-07-10T06:31:33.108Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/cf/00fe9414dfeafa6fe54eae9f5716c8c8e9ac59d192be3b893c096d395846/websockets-16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a71b73d143991714144e159f767b698f03c4a70b8a65ae1733b650cff488045b", size = 177472, upload-time = "2026-07-10T06:31:34.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/76/b10633424d40681b4e892ffd08ca5226322b2426e62d4ab71eae484c3a32/websockets-16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:187323204c3b2fc465e8fc2609e60437c521790cb9c1acb49c4c452a33e57f37", size = 177737, upload-time = "2026-07-10T06:31:35.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/61/d3bb03b2229bb1afd72008742d586cf1ea240dce64dd48c71c8c7fd3294c/websockets-16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9dba74233c8c3ce368850818c98354dad2570f57231b3fd3bd00d7aa57628881", size = 187403, upload-time = "2026-07-10T06:31:37.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/16/cc2e80478f688fc3c39c67dc1fac6a0783858058914ebc2489917462cb42/websockets-16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63339bc8c63c86a463177775cb7c677691f5bcfac7b3b2f01b286d42acd41600", size = 188639, upload-time = "2026-07-10T06:31:38.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/d6/ad87b2507e57de1cbf897a56c963f2925962ed5e85fbe06aaa83ced27acd/websockets-16.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23e545ea8ae4263e37cdfd4e22a217f519e48e432728bc461185bbf585f38a83", size = 190078, upload-time = "2026-07-10T06:31:40.218Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/1a/5b37b3fd335d5811f29fc829f2646a3e6d1463a4bf09c3100708684c766e/websockets-16.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2237081454846fb40403a80ba86d82e2038b9c45865ab96af0abe7d002a91045", size = 189267, upload-time = "2026-07-10T06:31:41.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/98/06afc33e9450d4230f94c664db78875d90f5f6a5fb77f0bc6ec15ae74e1c/websockets-16.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5f5218de1ed047385ca53744caba9435d65f75d008364970a3fae95a05812cf9", size = 188022, upload-time = "2026-07-10T06:31:42.838Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/bf/42fef5d5887c18cf2d148b02debf56cecb9cfbffc68027cde9b12c8f432c/websockets-16.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75c98e3920039d0edff03b74478ada504b7ce3a1bc406db2cabfca84320f7baf", size = 185435, upload-time = "2026-07-10T06:31:44.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/9b/8021c133add5fe40ed40312553a6cd1408c069d7efe3444ad483d4973ed3/websockets-16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1facd189d8190af30487a55b4c3688484dd50801628a3b5b2ccd26db08e67057", size = 188080, upload-time = "2026-07-10T06:31:45.986Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/54/1e37384f395eaa127383aab15c1c45e200890a7d7b99db5c312233d193e0/websockets-16.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:cc0c6a6eef613c7da32d4fb068f82ef834b58134f6a16b54e6c1e5bf9529ab3d", size = 186678, upload-time = "2026-07-10T06:31:47.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/79/1caeacab5bc2081e4519288d248bc8bd2de30652e6eaa94be6be09a1fe5b/websockets-16.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ad9411eded8988b879be6038206698bf7106c85a78f642c004485bcb95be17eb", size = 188554, upload-time = "2026-07-10T06:31:48.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/83/b3dca5fad71487b726e31cb0acf56f226792c1cc34e6ab18cbf146bd2d74/websockets-16.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cd68f0914f3b64694895bc5e9b14e8b447e41d7bf5ffaf989bb8dcb5e2dfdce7", size = 186109, upload-time = "2026-07-10T06:31:50.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/0b/8f246c3712f07f207b52ea5fb47f3b2b66fafec7303162644c74aed51c6a/websockets-16.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fef2debfe7f7ebdda12176f26166f95b7af17af05ba06150fcf889032e0213e9", size = 187061, upload-time = "2026-07-10T06:31:51.861Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/eb/27d6c92a01696b6495386af4fc941d7d0a13f2eab2bf9c336111d7321491/websockets-16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3cd6c9b798218798f4bb7b2e71c38f0e744bb94ca537b13376f88019d46384d", size = 187347, upload-time = "2026-07-10T06:31:53.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/d5/eeee439921f55d5eaeabcea18d0f7ce32cdc39cb8fc1e185431a094c5c7b/websockets-16.1-cp313-cp313-win32.whl", hash = "sha256:84c170c6869633536921e4474b1cce7254c0c9b0053ef5725f966cee47e718e4", size = 180149, upload-time = "2026-07-10T06:31:55.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/03/971e98d4a4864cf263f9e94c5b2b7c9a9b7682d77bfbba4e732c55ee85a9/websockets-16.1-cp313-cp313-win_amd64.whl", hash = "sha256:bef52d327d70fa75dad93ee61ea2cb1d1489aca9f35c188833563f5a3b4df0a5", size = 180458, upload-time = "2026-07-10T06:31:56.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/e6/da1dc11507f8118145a81c751fe0c77e5e1c11b8554496addb39389e2dc2/websockets-16.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f881fca0a45dd6789939bd6637cd98169b92f1c3fdc78262f2cb9ec2cb1f324e", size = 179833, upload-time = "2026-07-10T06:31:58.19Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/ac/c0d46f62e31e232487b2c123bc3cfd9a4e45684ca7dc0c37f0987f29baae/websockets-16.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:30c379d5b207d3a7f0ba4c2e4602a895b0bcc63fb5f5371a4ae7fbddb03b672b", size = 177524, upload-time = "2026-07-10T06:31:59.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/33/abd966074b34a51e4f134e0aaed80f5a4a0a35163ea5ac58a1bc5a076d23/websockets-16.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:98ab58a4faa72b46da0127ccc1931dcbfc0985b0778892300a092185910c4cbe", size = 177743, upload-time = "2026-07-10T06:32:00.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/30/646e47b8a8dff04e227bdab512e6dde60663a647eeac7bbd6edddd92bbc5/websockets-16.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e9c4e369fc181b2d41a99e01477215cecdc8546a39f7d41a59cc0a7065a0b09", size = 187474, upload-time = "2026-07-10T06:32:02.54Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/72/890ab9d77494af93ea65268230bfbc0a90ba789401ed7a44356a44785644/websockets-16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0704df094b2d5fa7f6f410925a594c2a5c9a09167731a76292e5410934208209", size = 188717, upload-time = "2026-07-10T06:32:04.156Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/aa/baedbbaa6bf9ed6029617ed5e8976535bd805f483ca9b3484e7ad9ee08bf/websockets-16.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b22b1f4950f6ab7126623329c3b47b3b90a14c05db517f2db2a026ad6c928352", size = 190090, upload-time = "2026-07-10T06:32:05.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/4f/d813ec94e18002571ef4959d87a630eff6e01b72a51bcb0832b75ae8c51a/websockets-16.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1ae4a686a662964a6671069f84f7f908cc3475e782227726b0c622c715962105", size = 189320, upload-time = "2026-07-10T06:32:07.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/3c/8ec52a6662f3df64090fba28cd521d405d54759268d8e820477037e8c80d/websockets-16.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:856bdd638f8277f86465057bfdd4da097c73058fb0f9d2bd5baea29e2bf2d367", size = 188068, upload-time = "2026-07-10T06:32:08.586Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/7f/f0ae6042b14f86fa5f996c6563ea4cf107adc036ccbedc9d4f418d0095f9/websockets-16.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9003a1fde1c21a322a3ca3fa0c4bda8c639da81dbc925162766086643b05ba87", size = 185493, upload-time = "2026-07-10T06:32:09.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/ad/5ffc53af9939c49fd653d147fa5b8f78ced1f6bce6c49a7446860945b0ce/websockets-16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39e947b1f5fdab045174306e3916785bf3ed537648acc1549827c08c33b10953", size = 188141, upload-time = "2026-07-10T06:32:11.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/62/729206c0ee577a4db8eae6dd06e0eef725a1287c6df11b2ef831d003df31/websockets-16.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5dd0e666b5931c0509cf65714686a1c5126771e663a79ac5d40da4f58b1f9502", size = 186653, upload-time = "2026-07-10T06:32:12.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/86/e8806a99ec4589914f255e6b658853fe537bf359c05e6ba5762ad9c27917/websockets-16.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a0285df7925657ad65a65fb8dc330808bce082827538fd50ef45fa12d1fc5bca", size = 188614, upload-time = "2026-07-10T06:32:14.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/38/ac554e2fc6ff0b8deeff9798b92e7abd8f99e2bd9731532e7033de208220/websockets-16.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:82d1c2cab3c133e9d059b3a5420bed9376bd30e21c185c63dda4ddadf6ddda47", size = 186165, upload-time = "2026-07-10T06:32:15.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/c5/4ef4d8e53342f94f3c49e1ae089b32c1e8b3878e15e0022c7708c647f351/websockets-16.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c39907f1eaf11f6277def65aa02d68f30576b693d0c1ca332aafa3caa723ac6d", size = 187119, upload-time = "2026-07-10T06:32:17.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/33/4788b1dd417bd97eeb2698af3b9df6775ac656f96e9987da0419a067602f/websockets-16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:45c5ea55446171949eb99fd34b771ceddd511ca21958d40d0197ced33159e5ee", size = 187411, upload-time = "2026-07-10T06:32:18.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/38/00d37aad6dc3244ce349e2864815362e50b3cfc00cac28d216db20efe40f/websockets-16.1-cp314-cp314-win32.whl", hash = "sha256:b8ef8b1c8d6bd029a475ac432e730fba2dfd456715d26c473e2a82291024b99c", size = 179822, upload-time = "2026-07-10T06:32:20.233Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/37/2a8cb0eaddee5eaebda47a90a3ba0898d1ce3d866b02a4857fea17d82e5b/websockets-16.1-cp314-cp314-win_amd64.whl", hash = "sha256:7358ff21632b5d062707f73e859c824f1c3807e73d8ca25e71caca7c4cdcf145", size = 180167, upload-time = "2026-07-10T06:32:21.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/5a/262ad5fcaef4198997b165060f09a63f861e76939b1786ab546ccc3f8120/websockets-16.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d0f38f4c3e9b359e257c339c2cc1967ccaeedb102e57c1c986bdce4bf4f32268", size = 180166, upload-time = "2026-07-10T06:32:23.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/c7/36377db690f4292826e4501a6dec2801dc55fd1cf0405923b04937e478df/websockets-16.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3c3d2cbd1602593bad49bd86fa3fbb25407d87a3b4bf8857c0ac5ac4914e1901", size = 177697, upload-time = "2026-07-10T06:32:25.164Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/c7/07171abce1e39799a76f473608580fe98bd43a1230f5146159622c02bccf/websockets-16.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:36069b74671e7e667f48a7484249f84c45a825a134c8b1bdc01875d0daa10d79", size = 177902, upload-time = "2026-07-10T06:32:26.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/17/c831f48e250bc4749f57c00dcce73337c41cd32f6d59a64567b84e782601/websockets-16.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:587f83c2ce8a5d628e166384d77fa7f0ac69b9007d515ab442123e6615aa8da3", size = 187766, upload-time = "2026-07-10T06:32:27.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/2e/4dfe63e245b0ecfaf470cf082d25c6ce35808159135fd88c82653a6b11ab/websockets-16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6db7972d52bc1b66cefe2246902e256cbaebc9ba8a45eac09343d7eb6671b2", size = 188939, upload-time = "2026-07-10T06:32:29.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/e5/5faf65aebd9562f6b4bc473d24ce38cc56f84eb5f5bee66ed9b86733f93c/websockets-16.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e7d6014888a0632e1ed7a4095248bb3095232999447f2d83bfb1900987dd9ed9", size = 191081, upload-time = "2026-07-10T06:32:30.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/cd/2634f2f2c0556c1aae6501ed6840019cc569dd6fdbcac6494378daea4dc0/websockets-16.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cb074d150e4ad2a77aa8a332c2be85f3f64f2681519d2570c1225c12c9821ff", size = 189513, upload-time = "2026-07-10T06:32:32.399Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/bb/2c700b51196104f09715b326b1f092ed25326bdf79a03e00a4842e503743/websockets-16.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d19c9067e1fe9490f974bffbc0e443b80a7674c5efb4980c429cc00771f07c5a", size = 188240, upload-time = "2026-07-10T06:32:33.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/20/86283636e499a1a357fa9441f690ba34f255e731f2fea174132b3b762b57/websockets-16.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d440ff0c6c7469ad59c0a412c383c235935b43635e89425e3f6a0c36de90c31b", size = 185955, upload-time = "2026-07-10T06:32:35.279Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/23/d7fb734b0095d43bc7f1c9f68afd50adb4176e7e513403e8c70ad7daa4fa/websockets-16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8613129a2533f08de24505e69a3e403cedaadae49abdb043c4d170ca71b7e4bd", size = 188491, upload-time = "2026-07-10T06:32:36.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/5e/168a192689db468405ecf3b8e4a2c18811936b0724d017ad7e6d252734f0/websockets-16.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:a5bf9c23f197b4ec88290fd5463f33db67362a1bb10f85fc2e8e7627f0ddab97", size = 186983, upload-time = "2026-07-10T06:32:38.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/9b/66795fa91ebe49019ebe4fa910282172252e37046b80e08fc52e0c365150/websockets-16.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:520b0fd0395f075febb283c76755af724ab9fd19dffa4f3bfd18cb4e622790a3", size = 188890, upload-time = "2026-07-10T06:32:39.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/32/126bbc844be5afb3613fd43211dac10a9645f4cf39741d04acaa2ec7030c/websockets-16.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7143aa09a67e1c013be44e81a88dfe90fc6244198ab86c7edd064152cf619805", size = 186583, upload-time = "2026-07-10T06:32:41.038Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/b9/0b5db9cbcf6e4970db4496893244a8d92e07f71a8ef27cf34b08aa02fef1/websockets-16.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:7acb811fad08e611755800d1560e395c67e11a6bd563598ea6abb319afb86938", size = 187353, upload-time = "2026-07-10T06:32:42.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/2e/254b2131a10d831b76e2c18dfe7add9729c6292c674a8085bf8de01ad151/websockets-16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c5cf88e3faa2f7931bc6baeee7599c97656a3f6ac7f831f4fccba233e141783a", size = 187784, upload-time = "2026-07-10T06:32:43.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/dc/e7288aa8e3ac5a88a0924619984d663c1abf2a87d0ea98290c66fdaee0ec/websockets-16.1-cp314-cp314t-win32.whl", hash = "sha256:589f8842521c8307684ce0b40ce4ad70c5e0aa46484c6f1225a94ef4b8970341", size = 179947, upload-time = "2026-07-10T06:32:45.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/de/37edf1260ff0fbbd2f82433489c4cfbe799ac2ff21355331609879329fe6/websockets-16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c0e0857c30bbbc2bb5c30687508f0b7ec19aa026cd9f2ff8424d0fee42dcc07", size = 180291, upload-time = "2026-07-10T06:32:47.119Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/58/bd83247f39ddc26ffc2c24eb05087a3b749e00cb4509fc6d19daa23c8495/websockets-16.1-py3-none-any.whl", hash = "sha256:c5149dfe490ec7e5ee5dbf624c642fb725f93a5575c7f00ab594ca9eddb8dd81", size = 174031, upload-time = "2026-07-10T06:32:56.079Z" },
|
||||
]
|
||||
24
web/.gitignore
vendored
Normal file
24
web/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
8
web/.oxlintrc.json
Normal file
8
web/.oxlintrc.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["react", "oxc"],
|
||||
"rules": {
|
||||
"react/rules-of-hooks": "error",
|
||||
"react/only-export-components": ["warn", { "allowConstantExport": true }]
|
||||
}
|
||||
}
|
||||
16
web/README.md
Normal file
16
web/README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the Oxlint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and Oxlint's TypeScript related rules in your project.
|
||||
13
web/index.html
Normal file
13
web/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>MODELBEAST</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
1393
web/package-lock.json
generated
Normal file
1393
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
web/package.json
Normal file
24
web/package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "web",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "oxlint",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"three": "^0.185.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"oxlint": "^1.71.0",
|
||||
"vite": "^8.1.1"
|
||||
}
|
||||
}
|
||||
1
web/public/favicon.svg
Normal file
1
web/public/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
24
web/public/icons.svg
Normal file
24
web/public/icons.svg
Normal file
@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||
</symbol>
|
||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||
</symbol>
|
||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||
</symbol>
|
||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||
</symbol>
|
||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
231
web/src/App.jsx
Normal file
231
web/src/App.jsx
Normal file
@ -0,0 +1,231 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
api, assetFileURL, connectWS, deleteAsset, listAssets, listJobs,
|
||||
listOperators, runJob, uploadFile,
|
||||
} from "./api";
|
||||
import Viewer from "./Viewer";
|
||||
|
||||
const MODEL_EXTS = ["glb", "gltf", "obj", "fbx", "ply", "stl"];
|
||||
const fmtSize = (n) =>
|
||||
n > 1e9 ? (n / 1e9).toFixed(1) + " GB" : n > 1e6 ? (n / 1e6).toFixed(1) + " MB"
|
||||
: n > 1e3 ? (n / 1e3).toFixed(0) + " KB" : n + " B";
|
||||
const KIND_ICON = { video: "🎬", image: "🖼", model: "🧊", frames: "🎞", other: "📄" };
|
||||
|
||||
function ParamForm({ schema, values, onChange }) {
|
||||
const props = schema?.properties || {};
|
||||
if (!Object.keys(props).length) return <p className="dim">No parameters.</p>;
|
||||
return (
|
||||
<div className="params">
|
||||
{Object.entries(props).map(([key, def]) => {
|
||||
const val = values[key] ?? def.default ?? "";
|
||||
const set = (v) => onChange({ ...values, [key]: v });
|
||||
return (
|
||||
<label key={key} title={def.description || ""}>
|
||||
<span>{key}</span>
|
||||
{def.enum ? (
|
||||
<select value={val} onChange={(e) => set(e.target.value)}>
|
||||
{def.enum.map((o) => <option key={o}>{o}</option>)}
|
||||
</select>
|
||||
) : def.type === "boolean" ? (
|
||||
<input type="checkbox" checked={!!val} onChange={(e) => set(e.target.checked)} />
|
||||
) : def.type === "number" || def.type === "integer" ? (
|
||||
<input type="number" value={val} step={def.type === "integer" ? 1 : "any"}
|
||||
min={def.minimum} max={def.maximum}
|
||||
onChange={(e) => set(def.type === "integer" ? parseInt(e.target.value || 0) : parseFloat(e.target.value || 0))} />
|
||||
) : (
|
||||
<input type="text" value={val} onChange={(e) => set(e.target.value)} />
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AssetPreview({ asset }) {
|
||||
const [folder, setFolder] = useState(null);
|
||||
const ext = asset ? asset.name.split(".").pop().toLowerCase() : "";
|
||||
useEffect(() => {
|
||||
setFolder(null);
|
||||
if (asset && (asset.kind === "frames" || asset.kind === "folder")) {
|
||||
api(assetFileURL(asset.id)).then(setFolder).catch(() => {});
|
||||
}
|
||||
}, [asset?.id]);
|
||||
|
||||
if (!asset) return <div className="preview empty">Select an asset — or drop / paste files anywhere</div>;
|
||||
if (MODEL_EXTS.includes(ext))
|
||||
return <Viewer url={assetFileURL(asset.id)} ext={ext} />;
|
||||
if (asset.kind === "image")
|
||||
return <div className="preview"><img src={assetFileURL(asset.id)} alt={asset.name} /></div>;
|
||||
if (asset.kind === "video")
|
||||
return <div className="preview"><video src={assetFileURL(asset.id)} controls /></div>;
|
||||
if (folder?.files)
|
||||
return (
|
||||
<div className="preview grid">
|
||||
{folder.files.slice(0, 60).map((f) => (
|
||||
<img key={f} src={assetFileURL(asset.id, f)} loading="lazy" alt={f} />
|
||||
))}
|
||||
{folder.files.length > 60 && <span className="dim">+{folder.files.length - 60} more</span>}
|
||||
</div>
|
||||
);
|
||||
if (ext === "json")
|
||||
return <JsonPreview url={assetFileURL(asset.id)} />;
|
||||
return <div className="preview empty">No preview for .{ext}</div>;
|
||||
}
|
||||
|
||||
function JsonPreview({ url }) {
|
||||
const [text, setText] = useState("…");
|
||||
useEffect(() => {
|
||||
fetch(url).then((r) => r.text()).then((t) => setText(t.slice(0, 20000)));
|
||||
}, [url]);
|
||||
return <pre className="preview json">{text}</pre>;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [assets, setAssets] = useState([]);
|
||||
const [operators, setOperators] = useState([]);
|
||||
const [jobs, setJobs] = useState([]);
|
||||
const [selected, setSelected] = useState(null);
|
||||
const [opId, setOpId] = useState(null);
|
||||
const [params, setParams] = useState({});
|
||||
const [openJob, setOpenJob] = useState(null);
|
||||
const fileInput = useRef(null);
|
||||
|
||||
const refreshAssets = useCallback(() => listAssets().then(setAssets), []);
|
||||
const refreshJobs = useCallback(() => listJobs().then(setJobs), []);
|
||||
|
||||
useEffect(() => {
|
||||
refreshAssets();
|
||||
refreshJobs();
|
||||
listOperators().then(setOperators);
|
||||
connectWS((msg) => {
|
||||
if (msg.type === "job") {
|
||||
setJobs((js) => {
|
||||
const i = js.findIndex((j) => j.id === msg.job.id);
|
||||
if (i === -1) return [msg.job, ...js];
|
||||
const copy = [...js]; copy[i] = msg.job; return copy;
|
||||
});
|
||||
}
|
||||
if (msg.type === "assets_changed") refreshAssets();
|
||||
});
|
||||
}, []);
|
||||
|
||||
// drag-drop + paste ingest
|
||||
useEffect(() => {
|
||||
const drop = async (e) => {
|
||||
e.preventDefault();
|
||||
for (const f of e.dataTransfer?.files || []) await uploadFile(f);
|
||||
refreshAssets();
|
||||
};
|
||||
const paste = async (e) => {
|
||||
const items = [...(e.clipboardData?.items || [])];
|
||||
for (const it of items) {
|
||||
const f = it.getAsFile?.();
|
||||
if (f) { await uploadFile(f); }
|
||||
}
|
||||
refreshAssets();
|
||||
};
|
||||
const prevent = (e) => e.preventDefault();
|
||||
window.addEventListener("drop", drop);
|
||||
window.addEventListener("dragover", prevent);
|
||||
window.addEventListener("paste", paste);
|
||||
return () => {
|
||||
window.removeEventListener("drop", drop);
|
||||
window.removeEventListener("dragover", prevent);
|
||||
window.removeEventListener("paste", paste);
|
||||
};
|
||||
}, [refreshAssets]);
|
||||
|
||||
const selectedAsset = assets.find((a) => a.id === selected) || null;
|
||||
const usableOps = useMemo(() =>
|
||||
operators.filter((op) => !selectedAsset || op.accepts.includes(selectedAsset.kind)),
|
||||
[operators, selectedAsset]);
|
||||
const activeOp = operators.find((o) => o.id === opId);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeOp) {
|
||||
const defaults = {};
|
||||
for (const [k, d] of Object.entries(activeOp.params_schema?.properties || {}))
|
||||
if (d.default !== undefined) defaults[k] = d.default;
|
||||
setParams(defaults);
|
||||
}
|
||||
}, [opId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (usableOps.length && !usableOps.find((o) => o.id === opId)) setOpId(usableOps[0].id);
|
||||
}, [usableOps]);
|
||||
|
||||
const launch = async () => {
|
||||
if (!activeOp || !selectedAsset) return;
|
||||
await runJob(activeOp.id, selectedAsset.id, params);
|
||||
refreshJobs();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="layout">
|
||||
<header>
|
||||
<h1>MODEL<span>BEAST</span></h1>
|
||||
<span className="dim">M3 Ultra · 256GB · local-first 3D factory</span>
|
||||
<button onClick={() => fileInput.current.click()}>+ Add files</button>
|
||||
<input ref={fileInput} type="file" multiple hidden
|
||||
onChange={async (e) => {
|
||||
for (const f of e.target.files) await uploadFile(f);
|
||||
e.target.value = ""; refreshAssets();
|
||||
}} />
|
||||
</header>
|
||||
|
||||
<aside className="assets">
|
||||
<h2>Assets</h2>
|
||||
{assets.map((a) => (
|
||||
<div key={a.id}
|
||||
className={"asset" + (a.id === selected ? " sel" : "")}
|
||||
onClick={() => setSelected(a.id)}>
|
||||
<span className="icon">{KIND_ICON[a.kind] || "📄"}</span>
|
||||
<div className="meta">
|
||||
<div className="name">{a.name}</div>
|
||||
<div className="dim">{a.kind} · {fmtSize(a.size)}</div>
|
||||
</div>
|
||||
<button className="x" title="delete" onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteAsset(a.id).then(refreshAssets);
|
||||
if (selected === a.id) setSelected(null);
|
||||
}}>×</button>
|
||||
</div>
|
||||
))}
|
||||
{!assets.length && <p className="dim">Drop videos, images, or 3D files anywhere.</p>}
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
<AssetPreview asset={selectedAsset} />
|
||||
</main>
|
||||
|
||||
<aside className="workbench">
|
||||
<h2>Run</h2>
|
||||
<select value={opId || ""} onChange={(e) => setOpId(e.target.value)}>
|
||||
{usableOps.map((op) => <option key={op.id} value={op.id}>{op.name}</option>)}
|
||||
</select>
|
||||
{activeOp && <p className="dim">{activeOp.description}</p>}
|
||||
{activeOp && <ParamForm schema={activeOp.params_schema} values={params} onChange={setParams} />}
|
||||
<button className="go" disabled={!selectedAsset || !activeOp} onClick={launch}>
|
||||
▶ Run {activeOp?.name || ""}
|
||||
</button>
|
||||
|
||||
<h2>Jobs</h2>
|
||||
<div className="jobs">
|
||||
{jobs.map((j) => (
|
||||
<div key={j.id} className={"job " + j.status}
|
||||
onClick={() => setOpenJob(openJob === j.id ? null : j.id)}>
|
||||
<span className="status">{j.status === "running" ? "⏳" : j.status === "done" ? "✅" : j.status === "error" ? "❌" : "🕐"}</span>
|
||||
<span>{j.operator}</span>
|
||||
<span className="dim">{j.id}</span>
|
||||
{openJob === j.id && (
|
||||
<pre className="log">{j.error ? `ERROR: ${j.error}\n` : ""}{j.log || "(no output yet)"}</pre>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!jobs.length && <p className="dim">No jobs yet.</p>}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
107
web/src/Viewer.jsx
Normal file
107
web/src/Viewer.jsx
Normal file
@ -0,0 +1,107 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import * as THREE from "three";
|
||||
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
||||
import { OBJLoader } from "three/addons/loaders/OBJLoader.js";
|
||||
import { FBXLoader } from "three/addons/loaders/FBXLoader.js";
|
||||
import { PLYLoader } from "three/addons/loaders/PLYLoader.js";
|
||||
import { STLLoader } from "three/addons/loaders/STLLoader.js";
|
||||
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
||||
|
||||
export default function Viewer({ url, ext }) {
|
||||
const mountRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const mount = mountRef.current;
|
||||
if (!mount || !url) return;
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x101014);
|
||||
const camera = new THREE.PerspectiveCamera(50, 1, 0.01, 1000);
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
mount.appendChild(renderer.domElement);
|
||||
|
||||
const resize = () => {
|
||||
const w = mount.clientWidth, h = mount.clientHeight;
|
||||
renderer.setSize(w, h);
|
||||
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
||||
camera.aspect = w / h;
|
||||
camera.updateProjectionMatrix();
|
||||
};
|
||||
resize();
|
||||
const ro = new ResizeObserver(resize);
|
||||
ro.observe(mount);
|
||||
|
||||
scene.add(new THREE.HemisphereLight(0xffffff, 0x334, 1.2));
|
||||
const dir = new THREE.DirectionalLight(0xffffff, 2.0);
|
||||
dir.position.set(3, 5, 4);
|
||||
scene.add(dir);
|
||||
const grid = new THREE.GridHelper(10, 20, 0x333344, 0x22222c);
|
||||
scene.add(grid);
|
||||
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
|
||||
let mixer = null;
|
||||
const clock = new THREE.Clock();
|
||||
|
||||
const frame = (obj) => {
|
||||
const box = new THREE.Box3().setFromObject(obj);
|
||||
const size = box.getSize(new THREE.Vector3()).length() || 1;
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
obj.position.sub(center);
|
||||
obj.position.y += (box.getSize(new THREE.Vector3()).y / 2);
|
||||
camera.position.set(size * 0.7, size * 0.5, size * 0.9);
|
||||
controls.target.set(0, box.getSize(new THREE.Vector3()).y / 4, 0);
|
||||
camera.near = size / 100;
|
||||
camera.far = size * 20;
|
||||
camera.updateProjectionMatrix();
|
||||
};
|
||||
|
||||
const addDefaultMaterial = (geometry) => {
|
||||
const hasColor = !!geometry.attributes.color;
|
||||
const mat = new THREE.MeshStandardMaterial({
|
||||
color: hasColor ? 0xffffff : 0x8899bb,
|
||||
vertexColors: hasColor,
|
||||
roughness: 0.7,
|
||||
});
|
||||
const mesh = new THREE.Mesh(geometry, mat);
|
||||
scene.add(mesh);
|
||||
frame(mesh);
|
||||
};
|
||||
|
||||
const onLoad = (obj) => {
|
||||
scene.add(obj.scene || obj);
|
||||
const target = obj.scene || obj;
|
||||
if (obj.animations?.length) {
|
||||
mixer = new THREE.AnimationMixer(target);
|
||||
mixer.clipAction(obj.animations[0]).play();
|
||||
}
|
||||
frame(target);
|
||||
};
|
||||
const err = (e) => console.error("viewer load error", e);
|
||||
|
||||
if (ext === "glb" || ext === "gltf") new GLTFLoader().load(url, onLoad, undefined, err);
|
||||
else if (ext === "obj") new OBJLoader().load(url, onLoad, undefined, err);
|
||||
else if (ext === "fbx") new FBXLoader().load(url, onLoad, undefined, err);
|
||||
else if (ext === "ply") new PLYLoader().load(url, (g) => { g.computeVertexNormals(); addDefaultMaterial(g); }, undefined, err);
|
||||
else if (ext === "stl") new STLLoader().load(url, (g) => { g.computeVertexNormals(); addDefaultMaterial(g); }, undefined, err);
|
||||
|
||||
let raf;
|
||||
const loop = () => {
|
||||
raf = requestAnimationFrame(loop);
|
||||
if (mixer) mixer.update(clock.getDelta());
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
loop();
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
ro.disconnect();
|
||||
renderer.dispose();
|
||||
mount.removeChild(renderer.domElement);
|
||||
};
|
||||
}, [url, ext]);
|
||||
|
||||
return <div className="viewer" ref={mountRef} />;
|
||||
}
|
||||
41
web/src/api.js
Normal file
41
web/src/api.js
Normal file
@ -0,0 +1,41 @@
|
||||
const BASE = "";
|
||||
|
||||
export async function api(path, opts = {}) {
|
||||
const res = await fetch(BASE + path, opts);
|
||||
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
|
||||
const ct = res.headers.get("content-type") || "";
|
||||
return ct.includes("json") ? res.json() : res;
|
||||
}
|
||||
|
||||
export const listAssets = () => api("/api/assets");
|
||||
export const listOperators = () => api("/api/operators");
|
||||
export const listJobs = () => api("/api/jobs");
|
||||
export const deleteAsset = (id) => api(`/api/assets/${id}`, { method: "DELETE" });
|
||||
|
||||
export async function uploadFile(file) {
|
||||
const form = new FormData();
|
||||
form.append("file", file, file.name || "pasted.png");
|
||||
return api("/api/assets", { method: "POST", body: form });
|
||||
}
|
||||
|
||||
export const runJob = (operator, asset_id, params) =>
|
||||
api("/api/jobs", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ operator, asset_id, params }),
|
||||
});
|
||||
|
||||
export function connectWS(onMessage) {
|
||||
const proto = location.protocol === "https:" ? "wss" : "ws";
|
||||
const ws = new WebSocket(`${proto}://${location.host}/ws`);
|
||||
ws.onmessage = (e) => onMessage(JSON.parse(e.data));
|
||||
const ping = setInterval(() => ws.readyState === 1 && ws.send("ping"), 20000);
|
||||
ws.onclose = () => {
|
||||
clearInterval(ping);
|
||||
setTimeout(() => connectWS(onMessage), 2000);
|
||||
};
|
||||
return ws;
|
||||
}
|
||||
|
||||
export const assetFileURL = (id, member) =>
|
||||
`/api/assets/${id}/file` + (member ? `?member=${encodeURIComponent(member)}` : "");
|
||||
1
web/src/assets/vite.svg
Normal file
1
web/src/assets/vite.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
74
web/src/index.css
Normal file
74
web/src/index.css
Normal file
@ -0,0 +1,74 @@
|
||||
* { box-sizing: border-box; margin: 0; }
|
||||
:root {
|
||||
--bg: #0b0b0f; --panel: #14141b; --panel2: #1b1b25; --edge: #26263a;
|
||||
--text: #e6e6f0; --dim: #8a8aa0; --accent: #7c5cff; --good: #3ddc84; --bad: #ff5c7a;
|
||||
}
|
||||
html, body, #root { height: 100%; }
|
||||
body {
|
||||
background: var(--bg); color: var(--text);
|
||||
font: 14px/1.45 -apple-system, "SF Pro Text", Helvetica, sans-serif;
|
||||
}
|
||||
.layout {
|
||||
display: grid; height: 100vh;
|
||||
grid-template: "header header header" 52px "assets main workbench" 1fr / 280px 1fr 340px;
|
||||
}
|
||||
header {
|
||||
grid-area: header; display: flex; align-items: center; gap: 14px;
|
||||
padding: 0 16px; border-bottom: 1px solid var(--edge); background: var(--panel);
|
||||
}
|
||||
h1 { font-size: 18px; letter-spacing: 2px; }
|
||||
h1 span { color: var(--accent); }
|
||||
h2 { font-size: 11px; text-transform: uppercase; letter-spacing: 1.5px; color: var(--dim); margin: 14px 0 8px; }
|
||||
header button { margin-left: auto; }
|
||||
button {
|
||||
background: var(--panel2); color: var(--text); border: 1px solid var(--edge);
|
||||
border-radius: 8px; padding: 7px 14px; cursor: pointer; font-size: 13px;
|
||||
}
|
||||
button:hover { border-color: var(--accent); }
|
||||
button.go {
|
||||
width: 100%; background: var(--accent); border: none; font-weight: 600;
|
||||
padding: 10px; margin-top: 12px;
|
||||
}
|
||||
button.go:disabled { opacity: 0.35; cursor: default; }
|
||||
.assets { grid-area: assets; overflow-y: auto; padding: 4px 12px; border-right: 1px solid var(--edge); }
|
||||
.workbench { grid-area: workbench; overflow-y: auto; padding: 4px 14px; border-left: 1px solid var(--edge); }
|
||||
main { grid-area: main; display: flex; min-width: 0; min-height: 0; }
|
||||
.dim { color: var(--dim); font-size: 12px; }
|
||||
.asset {
|
||||
display: flex; gap: 10px; align-items: center; padding: 8px; border-radius: 8px;
|
||||
cursor: pointer; border: 1px solid transparent;
|
||||
}
|
||||
.asset:hover { background: var(--panel); }
|
||||
.asset.sel { background: var(--panel2); border-color: var(--accent); }
|
||||
.asset .icon { font-size: 20px; }
|
||||
.asset .meta { min-width: 0; flex: 1; }
|
||||
.asset .name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 13px; }
|
||||
.asset .x { padding: 2px 8px; border: none; background: none; color: var(--dim); }
|
||||
.asset .x:hover { color: var(--bad); }
|
||||
.viewer, .preview { flex: 1; min-width: 0; }
|
||||
.preview { display: flex; align-items: center; justify-content: center; overflow: auto; padding: 16px; }
|
||||
.preview.empty { color: var(--dim); }
|
||||
.preview img, .preview video { max-width: 100%; max-height: 100%; border-radius: 8px; }
|
||||
.preview.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 6px; align-content: start; }
|
||||
.preview.grid img { width: 100%; border-radius: 4px; }
|
||||
.preview.json { font: 12px/1.5 "SF Mono", Menlo, monospace; color: #b8ffcf; align-self: stretch; text-align: left; white-space: pre-wrap; }
|
||||
.params label { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin: 7px 0; }
|
||||
.params span { font-size: 12.5px; color: var(--dim); }
|
||||
.params input[type="text"], .params input[type="number"], .params select, .workbench > select {
|
||||
background: var(--panel2); border: 1px solid var(--edge); color: var(--text);
|
||||
border-radius: 6px; padding: 6px 8px; width: 150px;
|
||||
}
|
||||
.workbench > select { width: 100%; }
|
||||
.jobs .job {
|
||||
padding: 8px; border-radius: 8px; margin-bottom: 6px; background: var(--panel);
|
||||
display: flex; gap: 8px; align-items: center; flex-wrap: wrap; cursor: pointer;
|
||||
border-left: 3px solid var(--edge); font-size: 13px;
|
||||
}
|
||||
.job.running { border-left-color: var(--accent); }
|
||||
.job.done { border-left-color: var(--good); }
|
||||
.job.error { border-left-color: var(--bad); }
|
||||
.job .log {
|
||||
flex-basis: 100%; font: 11px/1.5 "SF Mono", Menlo, monospace; color: var(--dim);
|
||||
max-height: 240px; overflow: auto; white-space: pre-wrap; background: var(--bg);
|
||||
padding: 8px; border-radius: 6px;
|
||||
}
|
||||
10
web/src/main.jsx
Normal file
10
web/src/main.jsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
7
web/vite.config.js
Normal file
7
web/vite.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user