diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f94b331 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,21 @@ +.git +.gitignore +.github +.venv +.env +.agents +.pytest_cache +.ruff_cache +__pycache__ +*.pyc +*.egg-info +.sessioniq-data +data +logs +docs +tests +web/node_modules +web/dist* +docker-compose.yml +Dockerfile +.dockerignore diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f028f96 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,20 @@ +# Keep LF endings in build and source files; CRLF breaks shell-style +# entrypoints and container builds. +*.py text eol=lf +*.ts text eol=lf +*.tsx text eol=lf +*.mjs text eol=lf +*.json text eol=lf +*.md text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.sh text eol=lf +*.conf text eol=lf +*.css text eol=lf +*.html text eol=lf +Dockerfile text eol=lf +.dockerignore text eol=lf + +*.png binary +*.ico binary +*.wav binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..948ed77 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,53 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + backend: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.11", "3.12"] + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: pyproject.toml + - run: pip install -e ".[dev]" + - run: ruff check . + - run: pytest -q + + frontend: + runs-on: ubuntu-latest + defaults: + run: + working-directory: web + steps: + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6 + with: + version: 12.4.2 + - uses: actions/setup-node@v7 + with: + node-version: 24 + cache: pnpm + cache-dependency-path: web/pnpm-lock.yaml + - run: pnpm install --frozen-lockfile + - run: pnpm test + - run: pnpm build + + docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - run: docker build -t sessioniq-api . + - run: docker build -t sessioniq-dashboard ./web diff --git a/.gitignore b/.gitignore index 6470687..b66eff9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ __pycache__/ .pytest_cache/ .ruff_cache/ -.streamlit/secrets.toml data/ data/uploads/ data/chroma/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f29855f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.12-slim + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + SESSIONIQ_HOST=0.0.0.0 \ + SESSIONIQ_PORT=8000 + +WORKDIR /app + +COPY pyproject.toml README.md ./ +COPY src ./src +RUN pip install --no-cache-dir . + +COPY scripts ./scripts + +EXPOSE 8000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/health', timeout=3)" + +CMD ["python", "scripts/run_api.py"] diff --git a/README.md b/README.md index 81a1c23..0a17b7d 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,12 @@ Upload audio, MIDI, and notes — SessionIQ analyzes them, organizes them into albums and songs, and answers questions about your work **with citations to the exact files it used.** +[![CI](https://github.com/xfznprojects/sessioniq/actions/workflows/ci.yml/badge.svg)](https://github.com/xfznprojects/sessioniq/actions/workflows/ci.yml) [![Python](https://img.shields.io/badge/Python-3.11+-3776AB?logo=python&logoColor=white)](https://www.python.org/) [![FastAPI](https://img.shields.io/badge/FastAPI-009688?logo=fastapi&logoColor=white)](https://fastapi.tiangolo.com/) [![React](https://img.shields.io/badge/React-19-61DAFB?logo=react&logoColor=black)](https://react.dev/) [![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-4-38BDF8?logo=tailwindcss&logoColor=white)](https://tailwindcss.com/) -[![Tests](https://img.shields.io/badge/tests-143_passing-2ea44f)](#-testing) [![Ruff](https://img.shields.io/badge/lint-ruff-261230?logo=ruff&logoColor=white)](https://docs.astral.sh/ruff/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) @@ -244,10 +244,31 @@ flowchart LR **Backend** — Python 3.11+, FastAPI, Uvicorn, Pydantic, librosa, numpy, soundfile, pretty_midi/mido, scipy (K-weighted loudness), ChromaDB (optional), faster-whisper (optional), OpenAI SDK (OpenAI **or** Ollama). **Frontend** — React 19, TypeScript, Vite, Tailwind CSS 4, TanStack Table, Recharts, HTML audio, Framer Motion, Lucide, music-metadata. -**Quality** — Pytest, Ruff, `tsc` + Vite build. +**Quality** — Pytest, Ruff, `tsc` + Vite build, GitHub Actions, Docker Compose. ## 🚀 Quick start +### Docker + +```bash +docker compose up --build +``` + +Then open **http://localhost:8080**. nginx serves the dashboard and proxies `/api` and `/uploads` +to the API container, so the browser sees a single origin. Runtime data lives in the +`sessioniq-data` volume and survives restarts. + +To load the generated demo content: + +```bash +docker compose exec api python scripts/seed_demo.py +docker compose restart api +``` + +To enable a model, uncomment `env_file: .env` in `docker-compose.yml` and put your keys in `.env`. + +### Local toolchain + **Prerequisites:** Python 3.11+ and Node 22.18+ (Node 24 recommended). > Paths below use Windows/PowerShell. On macOS/Linux use `.venv/bin/python` instead of `.venv\Scripts\python.exe`. @@ -264,8 +285,9 @@ python -m venv .venv ```powershell cd web -npm install # or: pnpm install -npm run dev # → http://127.0.0.1:5173 +corepack enable # provides the pnpm version pinned in package.json +pnpm install +pnpm dev # → http://127.0.0.1:5173 ``` **3. Load demo content** (optional, recommended) @@ -321,7 +343,7 @@ sessioniq/ ├── web/src/ # React + TypeScript dashboard │ ├── App.tsx │ └── components/ # Sidebar, Chat, Inspector, Insights, Studio, Pipeline, Player… -├── scripts/ # run_api, run_streamlit, seed_demo, check_local_ai +├── scripts/ # run_api, seed_demo, check_local_ai └── tests/ # pytest suite ``` @@ -330,10 +352,13 @@ sessioniq/ ```powershell .\.venv\Scripts\python.exe -m pytest # 143 passing .\.venv\Scripts\python.exe -m ruff check . -cd web; npm test # frontend scope regressions -npm run build # tsc + vite build +cd web; pnpm test # frontend scope regressions +pnpm build # tsc + vite build ``` +CI runs the backend suite on Python 3.11 and 3.12, the frontend tests and build, and both +container images on every push and pull request. + ## 🗄️ Library persistence and recovery SessionIQ is a single-process local app by design. Writes are serialized within the API process and diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ecab924 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +services: + api: + build: . + environment: + SESSIONIQ_HOST: 0.0.0.0 + SESSIONIQ_PORT: "8000" + volumes: + - sessioniq-data:/app/.sessioniq-data + ports: + - "8000:8000" + restart: unless-stopped + # Uncomment to pass model credentials through from a local .env file. + # env_file: .env + + web: + build: ./web + depends_on: + api: + condition: service_healthy + ports: + - "8080:80" + restart: unless-stopped + +volumes: + sessioniq-data: diff --git a/docs/UPDATE-LOG.md b/docs/UPDATE-LOG.md deleted file mode 100644 index eff541b..0000000 --- a/docs/UPDATE-LOG.md +++ /dev/null @@ -1,208 +0,0 @@ -# SessionIQ incremental update log - -## Step 1 — rollback failures (2026-08-31) - -User constraint: about 18% quota remained; take small steps and document work. Scope was deliberately limited to the first handoff review priority. All existing uncommitted changes were preserved. - -### Reproduced - -Added a regression that renames a two-file project, simulates an index-save failure, then simulates a locked file on the first rollback attempt. Before the fix, the rollback `PermissionError` replaced the original commit error and aborted the remaining recovery work. The new test failed on that exact behavior. - -### Changed - -- `src/sessioniq/api.py`: the mutation wrapper handles each filesystem rollback `OSError` separately and continues restoring other files. -- The original mutation/save exception is re-raised, with exception notes and error logging identifying any file requiring manual recovery. -- In-memory assets, task statuses, project order, preferences, and retriever state are restored in a `finally` block even if file restoration fails. -- Optional vectors remain disabled after a failed mutation, as before; restart rebuilds them from committed metadata. -- `tests/test_api.py`: added `test_failed_file_rollback_keeps_original_error_and_restores_other_state`. - -The regression verifies original exception identity, recovery-path notes, unchanged committed index bytes, restored asset metadata/retriever references, cleared file journal, successful restoration of the unaffected file, and preservation of the locked file at its moved location. - -### Verification - -```powershell -$env:PYTHON_DOTENV_DISABLED='1' -.\.venv\Scripts\python.exe -m pytest -q tests/test_api.py tests/test_safety.py -.\.venv\Scripts\python.exe -m ruff check --no-cache src/sessioniq/api.py tests/test_api.py -``` - -Result: **26 tests passed**, with one existing Starlette/httpx deprecation warning. Ruff passed. The newly added regression was observed failing before the implementation change and passing afterward. - -Tests used isolated temporary data. No UI/build checks were repeated because no frontend files changed. The previous full-suite/build verification remains recorded in the handoff; do not describe it as rerun in this step. - -### Limits and next small step - -- A genuinely locked/unavailable file cannot be restored automatically. Its bytes remain at the moved path, and the failure is identified in logs/exception notes. This change does not provide a persistent recovery queue or UI. -- Process crashes and multi-process consistency remain outside this fix. -- Existing preview/API processes were not restarted, so a running process may still have the previous wrapper loaded. -- No real-library files were changed. No commit or push was made. -- Suggested next small step: review the shared `FILE_MOVES` journal and choose an explicit nested-mutation policy or a request-local transaction object, with a focused regression. Do not launch a broad persistence rewrite or next-phase feature in the same step. - -## Step 2 — reject nested mutations (2026-08-31) - -User requested another small task with 14% quota left. Scope: enforce the previously documented non-nesting requirement without changing persistence architecture. - -### Reproduced and changed - -- Added `test_nested_mutation_rejected_and_outer_state_restored`, parameterized for metadata-only changes and file moves. Both cases failed before the fix because nested mutations were allowed to run and commit. -- `src/sessioniq/api.py`: added `_MUTATION_ACTIVE`, accessed under the existing `STATE_LOCK`. Re-entry raises `RuntimeError("Nested mutations are not supported")` before touching the snapshot/journal or running the inner function. -- The outer wrapper handles the uncaught error through its existing rollback path. Its `finally` resets the guard after success or failure. -- `@synchronized` remains reentrant, so mutation handlers can still assemble library responses. Other threads wait on the lock and are not rejected as nested calls. -- Regressions check that the inner body never executes, committed index bytes stay unchanged, original file bytes and asset metadata return, and a later normal project rename succeeds. - -### Verification - -```powershell -$env:PYTHON_DOTENV_DISABLED='1' -.\.venv\Scripts\python.exe -m pytest -q tests/test_api.py tests/test_safety.py -.\.venv\Scripts\python.exe -m ruff check --no-cache src/sessioniq/api.py tests/test_api.py -.\.venv\Scripts\python.exe -m ruff format --check src/sessioniq/api.py tests/test_api.py -git diff --check -``` - -Result: **28 tests passed**, with the existing Starlette/httpx deprecation warning. Ruff lint/format and diff whitespace checks passed; Git reported normal Windows line-ending notices. Both new regression cases were observed failing before the fix and passing afterward. - -### Handoff and limits - -- Nesting is rejected, not supported as a nested transaction/savepoint. Internal composition should use undecorated helpers under one outer mutation. -- The shared journal and lock still protect only one process. No database migration, durable crash recovery, or cross-process locking was added. -- Full backend suite, frontend build, and browser checks were not rerun for this backend-only step. Tests used isolated temporary data; real library files were untouched. -- Existing preview processes were not restarted. All previous uncommitted work remains; nothing was committed or pushed. -- A next small step could inspect persistent-player seek/resume timing and add one focused fix if reproduced. Read the existing player handoff notes first; defer broad persistence or version-comparison work to a larger session. - -## Step 3 — save explicit player seeks immediately (2026-08-31) - -User requested one more small task with 10% quota remaining. Scope: one player event handler; no backend or persistence redesign. - -### Finding and change - -Code inspection found that the seek slider only assigned `audio.currentTime`. Saving depended on a later `timeupdate`, throttled to once every two seconds, or a pause/end event. Seeking again while already paused within that window could leave the saved resume position stale indefinitely until another media event. - -`web/src/components/Player.tsx`: the slider change handler now also updates the displayed time and calls the existing `remember()` immediately after assigning the media position. Explicit seeks bypass the periodic save throttle. Storage failure remains nonfatal. Normal playback retains its existing periodic saves. - -### Verification and limits - -- `npm test`: **4 existing frontend scope tests passed**. These do not test audio behavior; no new automated player regression was added for this small handler change. -- `npm run build`: TypeScript and Vite production build passed. Vite emitted an informational plugin-timing notice; no build failure. Main bundle approximately 450.53 kB (138.21 kB gzip). -- `git diff --check`: passed, with existing Windows line-ending notices. -- Browser inspection used the isolated preview at `http://127.0.0.1:5174/`. It loaded 16 assets and restored `afterglow_master.wav` at approximately 4.85 seconds, paused. -- Browser slider keyboard/click/fill attempts did not change the media element's actual time. Fill changed the input's DOM value to 2.5, while the audio time and displayed label remained at approximately 4.85 seconds. This does **not** demonstrate a completed seek or a passing seek/refresh regression. The throttle gap is established by code inspection, not a reproduced browser timing failure. -- Manual acceptance still needed: while paused, seek twice within two seconds to different positions, refresh, and confirm the second position is restored on the same track. Also check seeking during playback. Cached metadata ordering and switching tracks during load remain separate open review items. -- No real-library files, backend code, dependencies, or server processes were changed. Nothing was committed or pushed. The preview tab was reloaded during inspection; browser-local resume state may be updated through ordinary player interactions. - -## Step 4 — focused review of remaining handoff priorities (2026-08-31) - -Independent review pass over the uncommitted update, covering handoff review priorities 3–9 (1 and 2 were already addressed in steps 1–2 and were re-verified only by running the suite). No implementation files were changed in this step. Evidence scripts live in the git-ignored `logs/review/`. - -### Verification of the current tree - -- `pytest -q`: **74 passed** (71 baseline + the three continuation regressions), same four deprecation warnings. -- `ruff check .`: passed. `web` `npm test`: **4 passed**. `npm run build`: passed, main bundle 450.53 kB / 138.21 kB gzip. -- `git status` unchanged; the real `.sessioniq-data` library was not touched. No commits, pushes, or server restarts. The isolated preview (8011/5174) was still running and was used for browser checks only. - -### Priority 3 — crash consistency (confirmed; two small gaps beyond the documented limitation) - -- Simulated a real kill mid-rename in a temp library: the mutation body ran (`os._exit` replaced the index save), then a fresh process re-imported the app. Result: the asset still lists under the **old** project name from the stale index, its media URL returns **404**, the file physically sits in the **renamed** project folder, and the only signal is a log warning ("Keeping metadata for unavailable file"). Recovery is manual, as documented. -- Additional gaps found: (a) crash-orphaned upload staging directories are never cleaned on restart; (b) the missing-file condition is not surfaced in the API payload or UI, so a library looks healthy until playback fails; (c) unlike deletions (trash manifests), renames leave no on-disk record of the intended destination, so no future tooling could reconcile automatically without a journal. -- Cheap mitigations before any durable-journal/SQLite redesign: reconcile at startup (match missing stored paths by filename + size elsewhere under UPLOAD_ROOT), add a `file_missing` flag to the asset payload, and purge stale `staging/` dirs at startup. - -### Priority 4 — performance at scale (confirmed and quantified) - -Measured with 1,200 synthetic assets of realistic shape (96-point audio series per `_downsample`, MIDI with 600 persisted notes, task-bearing notes) across 60 projects, in an isolated temp library: - -| Operation | Result | -| --- | --- | -| Index size on disk | 15.91 MB | -| Single task-status PATCH | **2.32–2.41 s** | -| — of which `copy.deepcopy` of all assets | ~1.32 s | -| — of which `save_index` (dumps + re-parse of previous index + `.bak`) | ~0.82 s | -| GET /api/library | ~110 ms / 5.1 MB | -| Retriever search (under `STATE_LOCK` in chat) | ~48 ms | - -- Every mutation pays the full-library deepcopy snapshot, a full index re-serialize, a full read+parse of the previous index (the `.bak` safety check), and returns the entire library in the response. At the current real-library size this is invisible; at a few hundred assets each checkbox toggle becomes noticeable, and the client's serialized mutation queue compounds it. -- MIDI note lists are persisted untruncated in the index (unlike the 32-note payload cap), which drove most of the 15.9 MB. -- Minor consistency note: `GET /api/assets/{id}/similar` and `/api/pipeline` iterate `ASSETS` without `STATE_LOCK`, unlike the other synchronized readers; worst case is a transient mixed view mid-mutation (GIL prevents corruption). - -### Priority 5 — numeric validation limits (both documented failure directions reproduced) - -- False negative: answer "Track beta.wav runs at 120 BPM" with alpha.wav (120 BPM) and beta.wav (90 BPM) both cited passes with zero errors — the answer-level check pools values across all cited assets. -- False positive: an answer quoting a note that legitimately says "target 140 BPM" produces two "Unsupported numeric claim: bpm=140" errors (evidence and answer levels), because note assets contribute no values to the pool. -- Control: "999 BPM" against a single 120 BPM asset is correctly rejected. Keep the limitation wording; if improved later, scope numbers to per-asset attribution patterns and allow numbers quoted from note text. - -### Priority 6 — player seek acceptance (completed; was open after step 3) - -Verified against the isolated preview via the browser runtime: - -- Paused: seek slider to 2.0 s then 4.0 s within two seconds — `sessioniq-resume` updated immediately to 4.0 (the step-3 fix working; previously the second seek inside the throttle window was not saved). Page reload restored the same track at exactly 4.0 s, paused. -- During playback: seek to 2.2 s saved instantly and playback continued advancing (~2.96 s after 800 ms). A seek while paused-at-end also saved instantly. -- Environment caveat: pointer/keyboard automation could not reach this in-app browser tab (locator click timed out; CUA click and key events never arrived in the page). Seeks were driven through the range input's native value setter plus a bubbling `input` event — the same React `onChange` handler a user drag fires. The reload/restore path was fully real. Cached-media event ordering and track switching during load remain open review items, not defects. - -### Priority 7 — unsaved notes (minor, acceptable) - -Drafts are per-asset, mount-initialized from `localStorage`, survive selection changes and refreshes, and are cleared only after a successful save. Queued client mutations each return a fresh full library, so state converges. Only a transient "Unsaved changes" flicker is possible when the post-upload `refreshLibrary` (not queued) races an in-flight note save. Cross-tab/cross-client last-writer-wins remains, consistent with the documented "not a synchronized notes database" caveat. - -### Priorities 8–9 — maintainability, docs and dependencies (confirmed) - -- `App.tsx` is 1,430 lines with ~21 components; `web/src/types.ts` still lacks the new optional `asset_id` fields (they pass through untyped today). -- `wavesurfer.js` remains in `web/package.json` (pinned to `"latest"`) with zero references in `web/src` — unused and a floating pin. Old screenshots in `docs/` predate the new UI. Both should be handled before or with a commit. - -### Suggested next small steps (in order) - -1. P4 mitigations if the real library may grow: per-asset undo snapshots instead of whole-library deepcopy, avoid re-parsing the previous index on every save, truncate persisted MIDI notes (or accept the size), and stop embedding the full library in every mutation response. -2. P3 cheap wins: startup reconciliation heuristic, `file_missing` flag in the payload, stale-staging cleanup. -3. Remove `wavesurfer.js`, refresh screenshots, then consider the feature phase from the handoff. - -## Step 5 — P3 small mitigation + P4 scale optimizations (2026-08-31) - -User decisions from the step 4 review: implement the small crash-consistency mitigation (P3), and apply the targeted performance optimizations because the library is expected to reach 1000+ files (P4). No durable journal / SQLite redesign was undertaken; all rollback and atomic-save guarantees are preserved. - -### P3 — crash recovery mitigation (`src/sessioniq/api.py`) - -- `_reconcile_missing_files()` runs at startup after the index load: an asset whose stored file is missing is relinked when exactly one **unreferenced** file with the same name exists under the uploads directory (the signature of a process killed between file moves and the index save). Files owned by other assets are never adopted, ambiguous names are skipped for manual recovery, and the asset keeps its original project grouping; the next move/rename tidies the folder. Recovery is in memory — startup still never rewrites the index. -- `_asset_payload()` now includes `file_missing`, and the inspector shows a red "File missing" badge (`web/src/types.ts` gained the optional field, previously noted as unaligned). -- `_clean_stale_staging()` removes upload staging directories older than 24 hours at startup, so a second API process pointed at the same data mid-upload is never disrupted. - -Verified end-to-end: the step 4 crash simulation (kill during rename, fresh process restart) now relinks the file automatically — the media URL resolves at the recovered location with HTTP 200 instead of 404. - -### P4 — mutation cost at scale - -Measured at 1,200 synthetic assets before → after: - -| Metric | Before | After | -| --- | --- | --- | -| Task-status PATCH | 2.32–2.41 s | **0.44–0.60 s** | -| Index size | 15.91 MB | **4.82 MB** | -| Index save (first/subsequent) | 486/793 ms | 308/322 ms | -| GET /api/library | 110 ms / 5.1 MB | 107 ms / 5.1 MB | - -Changes (`api.py`, `persistence.py`): - -- The mutation wrapper no longer deep-copies the whole library. It takes cheap reference snapshots of the asset list/order/preferences plus a **flat per-asset field record** (`_asset_field_snapshot`: status, tags copy, note, stored_path, project_name, file_name) for every asset. Nested analysis models are written once at ingest and never mutated in place, so copying them was pure overhead. Rollback restores list membership and all journaled fields — the existing nested-mutation test (which mutates `project_name` directly) still passes, proving direct field mutations remain covered. -- `persistence.save_index` keeps a known-good content cache: the previous index is only re-parsed when its bytes differ from what this process last wrote or loaded (external modification). The startup load registers the parsed index, so steady-state saves skip the 15 MB re-parse entirely. -- MIDI note lists are truncated to 32 entries in the saved index (matching the API payload cap); aggregates like `note_count` and pitch stats survive, and search/similarity never used more than 32 notes anyway. -- JSON is written with compact separators, and project artwork is resolved in one pass instead of rescanning all assets per project. -- Intentionally unchanged: every mutation still returns the full library (removing it would break ~20 test assertions and the client's `mutate()` for a modest gain now that the dominant costs are gone). - -### Regressions added (7; suite 74 → 81) - -- relink after simulated crash; no adoption of owned/ambiguous files; stale-staging cleanup age gate; `file_missing` flag true/false; failed asset update restores status/note/tags/fields; known-good index skips re-parse but still verifies externally modified bytes; saved index truncates MIDI notes while keeping counts. - -Two earlier test-authoring mistakes during this step are worth noting: the first rollback test assumed uploads store the note body in `asset.note` (it lives in `asset.text`) and asserted empty tags (note uploads are auto-tagged), and the reconciliation count assertions were brittle against leftovers from earlier tests in the shared temp library — including one benign case where reconciliation correctly recovered the locked-file test's deliberately abandoned file. - -### Verification - -`pytest -q`: **81 passed** (same four deprecation warnings). `ruff check`/`format` clean. `web`: `npm test` 4 passed; `npm run build` passed (main bundle 450.69 kB / 138.28 kB gzip). `git diff --check` clean apart from known Windows line-ending notices. Scale/crash numbers above re-measured via the isolated `logs/review/` harness; the real library, preview servers, and git history were untouched, and nothing was committed or pushed. - -### Remaining known limits - -- Reconciliation is a heuristic: cross-project same-name files stay missing; a partially completed rename can leave a project split across old/new names until tidied manually. -- A mutation still costs one full index serialization + write (≈0.3–0.6 s at ~1,200 assets); further gains would need an incremental/durable store — deferred by decision. -- Single-process guarantees unchanged; vectors still disabled after a failed mutation until restart. - -## Publication preparation (2026-08-31) - -The user explicitly requested pushing the completed work to `xfznprojects/sessioniq`. Verified `origin` points to that repository and fetched `origin/main`; it matched the local starting commit `48c645b`. - -Final checks on the completed source tree: **81 backend tests passed** (four existing dependency warnings), **4 frontend tests passed**, Ruff passed, and TypeScript/Vite production build passed. The first build attempt hit Windows `EPERM` when emptying the existing `web/dist/assets`; a fresh ignored output directory under `logs/` succeeded without deleting or unlocking existing files. Common credential-token/private-key pattern checks found no matches in the source/docs being published. Runtime data, `.env`, dependencies, logs, and build outputs remain excluded. - -The handoff's leading checkpoint now points to steps 4–5 so older review findings are not mistaken for the latest state. No new application behavior was added in publication preparation. Commit and push are authorized by the current user request; historical no-push statements above describe earlier steps. diff --git a/pyproject.toml b/pyproject.toml index 8531874..41f22c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + [project] name = "sessioniq" version = "0.1.0" @@ -15,8 +19,8 @@ dependencies = [ "pydantic>=2.7.0", "python-dotenv>=1.0.1", "python-multipart>=0.0.9", + "scipy>=1.11.0", "soundfile>=0.12.1", - "streamlit>=1.36.0", "uvicorn>=0.30.0", ] @@ -32,6 +36,9 @@ voice = [ "faster-whisper>=1.0.0", ] +[tool.setuptools.packages.find] +where = ["src"] + [tool.pytest.ini_options] testpaths = ["tests"] pythonpath = ["src"] diff --git a/scripts/run_api.py b/scripts/run_api.py index b32a1b9..04c0d3b 100644 --- a/scripts/run_api.py +++ b/scripts/run_api.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os import sys from pathlib import Path @@ -8,4 +9,8 @@ ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "src")) -uvicorn.run("sessioniq.api:app", host="127.0.0.1", port=8000) +uvicorn.run( + "sessioniq.api:app", + host=os.environ.get("SESSIONIQ_HOST", "127.0.0.1"), + port=int(os.environ.get("SESSIONIQ_PORT", "8000")), +) diff --git a/scripts/run_streamlit.py b/scripts/run_streamlit.py deleted file mode 100644 index 7f009dd..0000000 --- a/scripts/run_streamlit.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path - -from streamlit.web import cli as streamlit_cli - -ROOT = Path(__file__).resolve().parents[1] -APP_PATH = ROOT / "src" / "sessioniq" / "ui" / "streamlit_app.py" - -sys.path.insert(0, str(ROOT / "src")) -sys.argv = [ - "streamlit", - "run", - str(APP_PATH), - "--server.port", - "8501", - "--server.headless", - "true", - "--browser.gatherUsageStats", - "false", -] - -raise SystemExit(streamlit_cli.main()) diff --git a/src/sessioniq/audio_analysis.py b/src/sessioniq/audio_analysis.py index e993102..5abfc22 100644 --- a/src/sessioniq/audio_analysis.py +++ b/src/sessioniq/audio_analysis.py @@ -304,8 +304,9 @@ def integrated_lufs(samples: np.ndarray, sample_rate: int) -> float | None: return None try: - # scipy ships with librosa; lfilter is vectorized C, essential here — - # a per-sample Python loop would cost seconds per track. + # Imported locally so a missing scipy costs LUFS on this file rather than + # the whole analysis. lfilter is vectorized C — a per-sample Python loop + # would cost seconds per track. from scipy.signal import lfilter shelf_b, shelf_a = _high_shelf_coeffs(sample_rate) diff --git a/src/sessioniq/ui/__init__.py b/src/sessioniq/ui/__init__.py deleted file mode 100644 index f7e99a7..0000000 --- a/src/sessioniq/ui/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Streamlit UI for SessionIQ.""" diff --git a/src/sessioniq/ui/streamlit_app.py b/src/sessioniq/ui/streamlit_app.py deleted file mode 100644 index 7b805af..0000000 --- a/src/sessioniq/ui/streamlit_app.py +++ /dev/null @@ -1,228 +0,0 @@ -from __future__ import annotations - -import streamlit as st -from dotenv import load_dotenv - -from sessioniq.assistant import GroundedAssistant -from sessioniq.ingestion import ingest_file, supported_upload_types -from sessioniq.models import ProjectAsset, ProjectSummary, TaskStatus -from sessioniq.project_workspace import safe_upload_path, summarize_projects -from sessioniq.retrieval import InMemoryRetriever -from sessioniq.validation import validate_grounded_answer - - -def main() -> None: - load_dotenv() - st.set_page_config(page_title="SessionIQ", page_icon="SIQ", layout="wide") - _ensure_state() - - st.title("SessionIQ") - st.caption("Source-grounded AI assistant for music production sessions.") - - with st.sidebar: - st.header("Session Sources") - project_name = st.text_input( - "Project / song", - placeholder="e.g. Vivien remix, Album demos, Client cue 03", - ) - upload_note = st.text_area("Session note", placeholder="Optional context for these uploads") - uploads = st.file_uploader( - "Upload audio, MIDI, or notes", - type=supported_upload_types(), - accept_multiple_files=True, - ) - if st.button("Ingest uploads", type="primary", use_container_width=True): - _ingest_uploads(uploads, upload_note, project_name) - if st.button("Clear session", use_container_width=True): - st.session_state.assets = [] - st.session_state.messages = [] - st.session_state.task_statuses = {} - st.rerun() - - assets: list[ProjectAsset] = st.session_state.assets - summaries = summarize_projects(assets, st.session_state.task_statuses) - if assets: - projects = ["All projects", *[summary.project_name for summary in summaries]] - st.session_state.project_filter = st.selectbox( - "Workspace", - projects, - index=projects.index(st.session_state.project_filter) - if st.session_state.project_filter in projects - else 0, - ) - - visible_assets = _filtered_assets(assets) - visible_summaries = _filtered_summaries(summaries) - overview_tab, files_tab, chat_tab = st.tabs(["Projects", "Files", "Ask"]) - - with overview_tab: - _render_project_board(visible_summaries) - - with files_tab: - _render_assets(assets, visible_assets) - - with chat_tab: - st.subheader("Ask the Session") - for message in st.session_state.messages: - with st.chat_message(message["role"]): - st.markdown(message["content"]) - - question = st.chat_input("Ask about tempo, notes, levels, tasks, or arrangement details") - if question: - _answer_question(question, assets) - - -def _ensure_state() -> None: - st.session_state.setdefault("assets", []) - st.session_state.setdefault("messages", []) - st.session_state.setdefault("project_filter", "All projects") - st.session_state.setdefault("task_statuses", {}) - - -def _ingest_uploads(uploads: list | None, note: str, project_name: str) -> None: - if not uploads: - st.warning("Choose one or more files first.") - return - - created: list[ProjectAsset] = [] - with st.spinner("Analyzing sources..."): - for upload in uploads: - saved_path = safe_upload_path(project_name, upload.name) - saved_path.write_bytes(upload.getbuffer()) - try: - created.append( - ingest_file( - saved_path, - note=note, - project_name=project_name, - stored_path=str(saved_path), - ) - ) - except Exception as exc: - st.error(f"{upload.name}: {exc}") - - known_ids = {asset.id for asset in st.session_state.assets} - st.session_state.assets.extend(asset for asset in created if asset.id not in known_ids) - if created: - st.success(f"Ingested {len(created)} source file(s).") - - -def _filtered_assets(assets: list[ProjectAsset]) -> list[ProjectAsset]: - selected = st.session_state.get("project_filter", "All projects") - if selected == "All projects": - return assets - return [asset for asset in assets if asset.project_name == selected] - - -def _filtered_summaries(summaries: list[ProjectSummary]) -> list[ProjectSummary]: - selected = st.session_state.get("project_filter", "All projects") - if selected == "All projects": - return summaries - return [summary for summary in summaries if summary.project_name == selected] - - -def _render_project_board(summaries: list[ProjectSummary]) -> None: - st.subheader("Project Board") - if not summaries: - st.info("Upload files into a project to build the notebook.") - return - - for summary in summaries: - with st.container(border=True): - st.markdown(f"### {summary.project_name}") - metric_cols = st.columns(5) - metric_cols[0].metric("Files", summary.asset_count) - metric_cols[1].metric("Audio", summary.audio_count) - metric_cols[2].metric("MIDI", summary.midi_count) - metric_cols[3].metric("Notes", summary.note_count) - metric_cols[4].metric("Tasks", len(summary.tasks)) - st.progress(summary.progress, text=f"{round(summary.progress * 100)}% complete") - - if not summary.tasks: - st.caption( - "No action items found yet. Add notes like 'TODO', 'try', " - "'fix', or 'check'." - ) - continue - - for task in summary.tasks: - cols = st.columns([0.5, 0.24, 0.26]) - cols[0].markdown(f"{task.description} \n`{task.source_file}`") - status = cols[1].selectbox( - "Status", - [status.value for status in TaskStatus], - index=[status.value for status in TaskStatus].index(task.status.value), - key=f"task_status_{task.id}", - label_visibility="collapsed", - ) - st.session_state.task_statuses[task.id] = status - cols[2].caption(task.source_file) - - -def _render_assets(assets: list[ProjectAsset], visible_assets: list[ProjectAsset]) -> None: - st.subheader("Assets") - if not assets: - st.info("Upload source files to begin.") - return - if not visible_assets: - st.info("No files match the current workspace filter.") - return - - st.dataframe( - [_asset_row(asset) for asset in visible_assets], - use_container_width=True, - hide_index=True, - ) - with st.expander("Metadata", expanded=False): - selected = st.selectbox( - "Inspect file", - [asset.file_name for asset in visible_assets], - ) - asset = next(item for item in visible_assets if item.file_name == selected) - st.json(asset.compact_metadata()) - - -def _answer_question(question: str, assets: list[ProjectAsset]) -> None: - st.session_state.messages.append({"role": "user", "content": question}) - retriever = InMemoryRetriever() - retriever.add_assets(assets) - sources = retriever.search(question) - answer = GroundedAssistant().answer(question, sources) - errors = validate_grounded_answer(answer, sources) - - content = answer.answer - if answer.citations: - citations = "\n".join( - f"- `{citation.file_name}`: {citation.evidence}" for citation in answer.citations - ) - content = f"{content}\n\n**Sources**\n{citations}" - if errors: - content = f"{content}\n\n**Validation**\n" + "\n".join(f"- {error}" for error in errors) - - st.session_state.messages.append({"role": "assistant", "content": content}) - st.rerun() - - -def _asset_row(asset: ProjectAsset) -> dict[str, str | int | float | None]: - row: dict[str, str | int | float | None] = { - "Project": asset.project_name, - "File": asset.file_name, - "Type": asset.kind.value, - "Duration": None, - "BPM": None, - "Notes": None, - } - if asset.audio: - row["Duration"] = round(asset.audio.duration_seconds, 2) - row["BPM"] = round(asset.audio.bpm_estimate, 1) if asset.audio.bpm_estimate else None - if asset.midi: - row["Duration"] = round(asset.midi.duration_seconds, 2) - row["BPM"] = round(asset.midi.tempo_bpm, 1) if asset.midi.tempo_bpm else None - row["Notes"] = asset.midi.note_count - if asset.text: - row["Notes"] = asset.text.word_count - return row - - -if __name__ == "__main__": - main() diff --git a/web/.dockerignore b/web/.dockerignore new file mode 100644 index 0000000..bf222ce --- /dev/null +++ b/web/.dockerignore @@ -0,0 +1,3 @@ +node_modules +dist* +.pnpm-store diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..e469962 --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,19 @@ +FROM node:24-alpine AS build + +WORKDIR /app + +ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 +RUN corepack enable + +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +COPY . . +RUN pnpm build + +FROM nginx:alpine + +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 diff --git a/web/nginx.conf b/web/nginx.conf new file mode 100644 index 0000000..2cb3baf --- /dev/null +++ b/web/nginx.conf @@ -0,0 +1,37 @@ +server { + listen 80; + server_name _; + + # Folder import accepts large bounces, so the default 1m limit is far too low. + client_max_body_size 512M; + + root /usr/share/nginx/html; + index index.html; + + location /api/ { + proxy_pass http://api:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # The assistant streams answers as server-sent events; buffering would + # hold tokens until the response completed. + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 300s; + } + + location /uploads/ { + proxy_pass http://api:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/web/package.json b/web/package.json index 2da7d19..c058cf5 100644 --- a/web/package.json +++ b/web/package.json @@ -3,6 +3,7 @@ "private": true, "version": "0.1.0", "type": "module", + "packageManager": "pnpm@12.4.2", "scripts": { "dev": "vite --host 127.0.0.1 --port 5173", "build": "tsc && vite build", @@ -10,31 +11,26 @@ "preview": "vite preview --host 127.0.0.1 --port 5173" }, "dependencies": { - "@dnd-kit/core": "latest", - "@dnd-kit/sortable": "latest", - "@dnd-kit/utilities": "latest", - "@tanstack/react-table": "latest", - "class-variance-authority": "latest", - "clsx": "latest", - "framer-motion": "latest", - "lucide-react": "latest", - "music-metadata": "latest", - "react": "latest", - "react-arborist": "latest", - "react-dom": "latest", - "recharts": "latest", - "tailwind-merge": "latest", - "wavesurfer.js": "latest" + "@tanstack/react-table": "8.21.3", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "framer-motion": "12.42.2", + "lucide-react": "1.23.0", + "music-metadata": "11.13.0", + "react": "19.2.7", + "react-dom": "19.2.7", + "recharts": "3.9.2", + "tailwind-merge": "3.6.0" }, "devDependencies": { - "@tailwindcss/postcss": "^4.3.2", - "@types/react": "latest", - "@types/react-dom": "latest", - "@vitejs/plugin-react": "latest", - "autoprefixer": "latest", - "postcss": "latest", - "tailwindcss": "latest", - "typescript": "latest", - "vite": "latest" + "@tailwindcss/postcss": "4.3.2", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-react": "6.0.3", + "autoprefixer": "10.5.2", + "postcss": "8.5.16", + "tailwindcss": "4.3.2", + "typescript": "6.0.3", + "vite": "8.1.3" } } diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 02fc7a1..e316d57 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -1,3 +1,161 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: {} + packageManagerDependencies: + pnpm: + specifier: 12.4.2 + version: 12.4.2 + +packages: + + '@pnpm/exe.android-arm64@12.4.2': + resolution: {integrity: sha512-E255MbcQ0V1577M2BV0ajWuP7KmTPYA0jqZnk3rK6Lq3kTvZWulMMb7iqRmnLsQbyWTkMEB2CfyM70loVDA8xg==} + cpu: [arm64] + os: [android] + + '@pnpm/exe.android-x64@12.4.2': + resolution: {integrity: sha512-J1pSeCUwuKxMG70ZzpWn8JzElxiEa8NN/3N0SlZRtU2xbztChaJCKF3HaBNIj9yPfeofWzJo/lwNvDDjraQuZw==} + cpu: [x64] + os: [android] + + '@pnpm/exe.darwin-arm64@12.4.2': + resolution: {integrity: sha512-A0WDo8iErfZBXgrLseQxw8i8Y9ctUpOEl/Uu+cubnTzpD8tT9ykIB548L8YTM2WD4OS+ZOHSxy8aGZcvKq8PaQ==} + cpu: [arm64] + os: [darwin] + + '@pnpm/exe.darwin-x64@12.4.2': + resolution: {integrity: sha512-MSgJdovBWHcb5DEOvfPH9yNi/T5O1Xa4ess+E1ESGo/5yuty7S4JoiIjami+fsNXfnoQMlwsMUqYU4zAvBcNCw==} + cpu: [x64] + os: [darwin] + + '@pnpm/exe.freebsd-x64@12.4.2': + resolution: {integrity: sha512-h2YumlQSNvgbRPv+RXwABohX65f9bOBZn+jMIt7bFDISZPCzQ+Nvpt6Awbp4ip5PwQgYxbu5iREJ1fHE39Fm8A==} + cpu: [x64] + os: [freebsd] + + '@pnpm/exe.linux-arm64-musl@12.4.2': + resolution: {integrity: sha512-LwSEtSEDTv6S51YLs3YvSkPyun/QmfMic1UGICUkPWFu6ByP43RdMlkKvmVkfGhAYCpnxO057vrmyJqtfZrPCA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-arm64@12.4.2': + resolution: {integrity: sha512-2dSiDXyhx+RTHsewxex8f/jVjqQXWJ2oow4kCVHEWdZKeBpgMxvZ6fHkTAUBJXgqhbKIDHvuNlZBP7gJfUWL5A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-ppc64@12.4.2': + resolution: {integrity: sha512-8Itc+jQk+MTz04LS9D1RcH+VctAWmzM4l1cJQ+Sx7pAJNo7EKHdRMbC0Tok1jyQ7eEHNJZD5EleYneZqWfZM+g==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-riscv64@12.4.2': + resolution: {integrity: sha512-hleOeqhTVpH+z9RVMGnxvU4ZnrkBUClWjCbHD9u6kwyqhGSpevoU1wTGish+CBRhmIgMAy9pAfFpqhbAKOKNfw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-s390x@12.4.2': + resolution: {integrity: sha512-LAsQRRdP9aToENR6dtcIJ9l+e1zMYOX0tQcLGpRyLPVBQcYRLlvAPcmDshsiIHQjp05SDa9FI0czX1ZQ1a7a/A==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-x64-musl@12.4.2': + resolution: {integrity: sha512-kzfzH2/0BWdTABK14Yj5a1xsdkTEQUp2eXEPNakaD9jKL025lq3hyaKHIz/gIZaPDMe/1bFFK/En4ztFlbBJxw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-x64@12.4.2': + resolution: {integrity: sha512-/pbt0UVTa8NMDhzOWLQRfZ6G9ROKXlJPZtx845BqyWfc7hCrWXhTLBd70yO2y8+E+IWN3oHM1s/JsIQNGk1yvg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.win32-arm64@12.4.2': + resolution: {integrity: sha512-PsW19e4dAUNpZ0cS9flaxFuAmpt2dKlH/Vvi8TZ4qyJjjQzue/CEsWm+6wKVP4CJ7IT8RdL4qh+soOPWIgF2Zw==} + cpu: [arm64] + os: [win32] + + '@pnpm/exe.win32-x64@12.4.2': + resolution: {integrity: sha512-+xGoeE0g55ztWvl8i5QqdmNfW3nIrTVcoQrNshEOwxthm9Ag48oXton3uxOA3/SANyz5AXexEjj2KO20NnOrEw==} + cpu: [x64] + os: [win32] + + pnpm@12.4.2: + resolution: {integrity: sha512-CK3GYTGAJ1x8ntraOdzwjJxhrU5+rzMKTzRh8QKw+QdCNFTRF/mOctR/7wYWBwZE17/8lzpqV/UJCm18NosHyQ==} + engines: {node: '>=18.*'} + hasBin: true + +snapshots: + + '@pnpm/exe.android-arm64@12.4.2': + optional: true + + '@pnpm/exe.android-x64@12.4.2': + optional: true + + '@pnpm/exe.darwin-arm64@12.4.2': + optional: true + + '@pnpm/exe.darwin-x64@12.4.2': + optional: true + + '@pnpm/exe.freebsd-x64@12.4.2': + optional: true + + '@pnpm/exe.linux-arm64-musl@12.4.2': + optional: true + + '@pnpm/exe.linux-arm64@12.4.2': + optional: true + + '@pnpm/exe.linux-ppc64@12.4.2': + optional: true + + '@pnpm/exe.linux-riscv64@12.4.2': + optional: true + + '@pnpm/exe.linux-s390x@12.4.2': + optional: true + + '@pnpm/exe.linux-x64-musl@12.4.2': + optional: true + + '@pnpm/exe.linux-x64@12.4.2': + optional: true + + '@pnpm/exe.win32-arm64@12.4.2': + optional: true + + '@pnpm/exe.win32-x64@12.4.2': + optional: true + + pnpm@12.4.2: + optionalDependencies: + '@pnpm/exe.android-arm64': 12.4.2 + '@pnpm/exe.android-x64': 12.4.2 + '@pnpm/exe.darwin-arm64': 12.4.2 + '@pnpm/exe.darwin-x64': 12.4.2 + '@pnpm/exe.freebsd-x64': 12.4.2 + '@pnpm/exe.linux-arm64': 12.4.2 + '@pnpm/exe.linux-arm64-musl': 12.4.2 + '@pnpm/exe.linux-ppc64': 12.4.2 + '@pnpm/exe.linux-riscv64': 12.4.2 + '@pnpm/exe.linux-s390x': 12.4.2 + '@pnpm/exe.linux-x64': 12.4.2 + '@pnpm/exe.linux-x64-musl': 12.4.2 + '@pnpm/exe.win32-arm64': 12.4.2 + '@pnpm/exe.win32-x64': 12.4.2 + +--- lockfileVersion: '9.0' settings: @@ -8,78 +166,63 @@ importers: .: dependencies: - '@dnd-kit/core': - specifier: latest - version: 6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@dnd-kit/sortable': - specifier: latest - version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) - '@dnd-kit/utilities': - specifier: latest - version: 3.2.2(react@19.2.7) '@tanstack/react-table': - specifier: latest + specifier: 8.21.3 version: 8.21.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) class-variance-authority: - specifier: latest + specifier: 0.7.1 version: 0.7.1 clsx: - specifier: latest + specifier: 2.1.1 version: 2.1.1 framer-motion: - specifier: latest + specifier: 12.42.2 version: 12.42.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) lucide-react: - specifier: latest + specifier: 1.23.0 version: 1.23.0(react@19.2.7) music-metadata: - specifier: latest + specifier: 11.13.0 version: 11.13.0 react: - specifier: latest + specifier: 19.2.7 version: 19.2.7 - react-arborist: - specifier: latest - version: 3.13.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react-dom: - specifier: latest + specifier: 19.2.7 version: 19.2.7(react@19.2.7) recharts: - specifier: latest + specifier: 3.9.2 version: 3.9.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@16.13.1)(react@19.2.7)(redux@5.0.1) tailwind-merge: - specifier: latest + specifier: 3.6.0 version: 3.6.0 - wavesurfer.js: - specifier: latest - version: 7.12.10 devDependencies: '@tailwindcss/postcss': - specifier: ^4.3.2 + specifier: 4.3.2 version: 4.3.2 '@types/react': - specifier: latest + specifier: 19.2.17 version: 19.2.17 '@types/react-dom': - specifier: latest + specifier: 19.2.3 version: 19.2.3(@types/react@19.2.17) '@vitejs/plugin-react': - specifier: latest + specifier: 6.0.3 version: 6.0.3(vite@8.1.3(jiti@2.7.0)) autoprefixer: - specifier: latest + specifier: 10.5.2 version: 10.5.2(postcss@8.5.16) postcss: - specifier: latest + specifier: 8.5.16 version: 8.5.16 tailwindcss: - specifier: latest + specifier: 4.3.2 version: 4.3.2 typescript: - specifier: latest + specifier: 6.0.3 version: 6.0.3 vite: - specifier: latest + specifier: 8.1.3 version: 8.1.3(jiti@2.7.0) packages: @@ -88,35 +231,9 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@babel/runtime@7.29.7': - resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} - engines: {node: '>=6.9.0'} - '@borewit/text-codec@0.2.2': resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} - '@dnd-kit/accessibility@3.1.1': - resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} - peerDependencies: - react: '>=16.8.0' - - '@dnd-kit/core@6.3.1': - resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@dnd-kit/sortable@10.0.0': - resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} - peerDependencies: - '@dnd-kit/core': ^6.3.0 - react: '>=16.8.0' - - '@dnd-kit/utilities@3.2.2': - resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} - peerDependencies: - react: '>=16.8.0' - '@emnapi/core@1.11.1': resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} @@ -151,15 +268,6 @@ packages: '@oxc-project/types@0.138.0': resolution: {integrity: sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==} - '@react-dnd/asap@4.0.1': - resolution: {integrity: sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==} - - '@react-dnd/invariant@2.0.0': - resolution: {integrity: sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==} - - '@react-dnd/shallowequal@2.0.0': - resolution: {integrity: sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==} - '@reduxjs/toolkit@2.12.0': resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==} peerDependencies: @@ -533,9 +641,6 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - dnd-core@14.0.1: - resolution: {integrity: sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==} - electron-to-chromium@1.5.387: resolution: {integrity: sha512-TaxwufTFDufvPEoXdhwVrA3UdFWBeWGkYoJ1K8ldF1xe6gKfth6iRNS5lTQ5JPNOHdGQm8PT1QYKUqFLCiUefQ==} @@ -553,9 +658,6 @@ packages: eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -594,9 +696,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - hoist-non-react-statics@3.3.2: - resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -697,9 +796,6 @@ packages: resolution: {integrity: sha512-kOy3OxT2HH39N70UnKgu4NWDZjLOz8W/mfyvniHjRH/DrL3f2pOfvWQ4p60offbbtDAnXWp0v9LfMIqMec269Q==} engines: {node: '>=18'} - memoize-one@5.2.1: - resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} - motion-dom@12.42.2: resolution: {integrity: sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==} @@ -736,30 +832,6 @@ packages: resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} - react-arborist@3.13.2: - resolution: {integrity: sha512-Qa5h2MwBGflTZXsg8vXOHOonF04w31u6DyPL06iyboe+A8CMqxRxgI/Imd5mwTo679iNNJ4e7CuinJGe3rGqgg==} - peerDependencies: - react: '>= 16.14' - react-dom: '>= 16.14' - - react-dnd-html5-backend@14.1.0: - resolution: {integrity: sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==} - - react-dnd@14.0.5: - resolution: {integrity: sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A==} - peerDependencies: - '@types/hoist-non-react-statics': '>= 3.3.1' - '@types/node': '>= 12' - '@types/react': '>= 16' - react: '>= 16.14' - peerDependenciesMeta: - '@types/hoist-non-react-statics': - optional: true - '@types/node': - optional: true - '@types/react': - optional: true - react-dom@19.2.7: resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} peerDependencies: @@ -780,13 +852,6 @@ packages: redux: optional: true - react-window@1.8.11: - resolution: {integrity: sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==} - engines: {node: '>8.0.0'} - peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react@19.2.7: resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} @@ -804,9 +869,6 @@ packages: peerDependencies: redux: ^5.0.0 - redux@4.2.1: - resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} - redux@5.0.1: resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} @@ -919,9 +981,6 @@ packages: yaml: optional: true - wavesurfer.js@7.12.10: - resolution: {integrity: sha512-M3AC5biFmvfy7Oxe8FN6I88H0z1c8Vuz81N6Oq2CY4kOumNDb6U1/0qg4pomjP619vJqdWADlgUcpOKvfGifng==} - win-guid@0.2.1: resolution: {integrity: sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A==} @@ -929,35 +988,8 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@babel/runtime@7.29.7': {} - '@borewit/text-codec@0.2.2': {} - '@dnd-kit/accessibility@3.1.1(react@19.2.7)': - dependencies: - react: 19.2.7 - tslib: 2.8.1 - - '@dnd-kit/core@6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@dnd-kit/accessibility': 3.1.1(react@19.2.7) - '@dnd-kit/utilities': 3.2.2(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - tslib: 2.8.1 - - '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': - dependencies: - '@dnd-kit/core': 6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@dnd-kit/utilities': 3.2.2(react@19.2.7) - react: 19.2.7 - tslib: 2.8.1 - - '@dnd-kit/utilities@3.2.2(react@19.2.7)': - dependencies: - react: 19.2.7 - tslib: 2.8.1 - '@emnapi/core@1.11.1': dependencies: '@emnapi/wasi-threads': 1.2.2 @@ -1002,12 +1034,6 @@ snapshots: '@oxc-project/types@0.138.0': {} - '@react-dnd/asap@4.0.1': {} - - '@react-dnd/invariant@2.0.0': {} - - '@react-dnd/shallowequal@2.0.0': {} - '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7)': dependencies: '@standard-schema/spec': 1.1.0 @@ -1282,12 +1308,6 @@ snapshots: detect-libc@2.1.2: {} - dnd-core@14.0.1: - dependencies: - '@react-dnd/asap': 4.0.1 - '@react-dnd/invariant': 2.0.0 - redux: 4.2.1 - electron-to-chromium@1.5.387: {} enhanced-resolve@5.21.6: @@ -1301,8 +1321,6 @@ snapshots: eventemitter3@5.0.4: {} - fast-deep-equal@3.1.3: {} - fdir@6.5.0(picomatch@4.0.5): optionalDependencies: picomatch: 4.0.5 @@ -1332,10 +1350,6 @@ snapshots: graceful-fs@4.2.11: {} - hoist-non-react-statics@3.3.2: - dependencies: - react-is: 16.13.1 - ieee754@1.2.1: {} immer@11.1.11: {} @@ -1403,8 +1417,6 @@ snapshots: media-typer@2.0.0: {} - memoize-one@5.2.1: {} - motion-dom@12.42.2: dependencies: motion-utils: 12.39.0 @@ -1444,35 +1456,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - react-arborist@3.13.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): - dependencies: - react: 19.2.7 - react-dnd: 14.0.5(@types/react@19.2.17)(react@19.2.7) - react-dnd-html5-backend: 14.1.0 - react-dom: 19.2.7(react@19.2.7) - react-window: 1.8.11(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - redux: 5.0.1 - use-sync-external-store: 1.6.0(react@19.2.7) - transitivePeerDependencies: - - '@types/hoist-non-react-statics' - - '@types/node' - - '@types/react' - - react-dnd-html5-backend@14.1.0: - dependencies: - dnd-core: 14.0.1 - - react-dnd@14.0.5(@types/react@19.2.17)(react@19.2.7): - dependencies: - '@react-dnd/invariant': 2.0.0 - '@react-dnd/shallowequal': 2.0.0 - dnd-core: 14.0.1 - fast-deep-equal: 3.1.3 - hoist-non-react-statics: 3.3.2 - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - react-dom@19.2.7(react@19.2.7): dependencies: react: 19.2.7 @@ -1489,13 +1472,6 @@ snapshots: '@types/react': 19.2.17 redux: 5.0.1 - react-window@1.8.11(react-dom@19.2.7(react@19.2.7))(react@19.2.7): - dependencies: - '@babel/runtime': 7.29.7 - memoize-one: 5.2.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react@19.2.7: {} recharts@3.9.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@16.13.1)(react@19.2.7)(redux@5.0.1): @@ -1522,10 +1498,6 @@ snapshots: dependencies: redux: 5.0.1 - redux@4.2.1: - dependencies: - '@babel/runtime': 7.29.7 - redux@5.0.1: {} reselect@5.2.0: {} @@ -1622,6 +1594,4 @@ snapshots: fsevents: 2.3.3 jiti: 2.7.0 - wavesurfer.js@7.12.10: {} - win-guid@0.2.1: {}