Compare commits
149 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4789f73ae7 | |||
| 24051a2794 | |||
| 3fc2087ee1 | |||
| 19d9bf3c9e | |||
| e9c710ec0b | |||
| e96aecfdf1 | |||
| f9438b10b6 | |||
| bc759dd99a | |||
| 3f35b84535 | |||
| 6edf526d27 | |||
| fc24b08505 | |||
| 0f78093ce2 | |||
| 029ed1c456 | |||
| 9ef622f29a | |||
| 8e3c652551 | |||
| 393df8fd7b | |||
| 688a7a57e2 | |||
| 652e745d0a | |||
| 45ab31d42e | |||
| c3182f6baa | |||
| 1d9b426d15 | |||
| 4f34491f3e | |||
| 9e269d3b79 | |||
| 58c6ec794d | |||
| 173a1f227b | |||
| c1aa1eb4d4 | |||
| 6c16bf8410 | |||
| 4981785913 | |||
| bca661bffd | |||
| 4c820bfa77 | |||
| 8cdf2942b9 | |||
| 23675dab83 | |||
| 71b9231b57 | |||
| 93dc13b17a | |||
| f1e914fa62 | |||
| 4dfab954d8 | |||
| e99e54773f | |||
| 0ba31c57bf | |||
| 454dd91f96 | |||
| e9f29e09ca | |||
| 289746f187 | |||
| b3ced7a7b3 | |||
| 4233c1eeda | |||
| 35f7c3822f | |||
| d37f4e7557 | |||
| 8a9d6b3c27 | |||
| 55d3464b1e | |||
| cfc114b9d7 | |||
| 856a8183b6 | |||
| 0272912cff | |||
| c047934fc4 | |||
| fc663921ff | |||
| fe5e8cb58e | |||
| 01fcf51fea | |||
| 1af2119444 | |||
| e5b4321a55 | |||
| ec5fedec33 | |||
| 8c1a8ead56 | |||
| ed91e4534c | |||
| 8fc1c2641b | |||
| a9da813cb1 | |||
| 6f55d36085 | |||
| 7325fbfc45 | |||
| 94b2d2ef41 | |||
| f616edce49 | |||
| 6018263044 | |||
| 36edc9daca | |||
| 71278f2aba | |||
| 022ba38a5e | |||
| 71c3bafdb5 | |||
| 34ad02dd6b | |||
| 47b1633bd3 | |||
| 0182abf7ea | |||
| 9b6de7f857 | |||
| 84ab4ae823 | |||
| 25471e6ea5 | |||
| 0dc95386f2 | |||
| d8227904b6 | |||
| 4d10b9485b | |||
| adccf6430a | |||
| 73025fc387 | |||
| 9f762c2a78 | |||
| a0b6110fd1 | |||
| e88ce2e3ea | |||
| 827693dfc4 | |||
| d04631c7d3 | |||
| 224fd54a56 | |||
| befc0d35fd | |||
| 5e3a283bd9 | |||
| 4a04bc04ef | |||
| dc384b3102 | |||
| 0b9079632a | |||
| 9be6991222 | |||
| 0853a13f62 | |||
| c09f994ede | |||
| 249e2afea2 | |||
| b65188fcc8 | |||
| ba5883951d | |||
| 7000529d8d | |||
| 1de6bd6b57 | |||
| c435d00897 | |||
| b075a5eca7 | |||
| 36ee2bdf90 | |||
| 5423686c73 | |||
| 4db777eb5d | |||
| 1989bfb105 | |||
| d1995307f5 | |||
| f647291523 | |||
| fdaabe3a08 | |||
| 548ab1258f | |||
| 2c2ee59d7d | |||
| 7e5f406226 | |||
| ad5cda5a40 | |||
| 05e6a467d8 | |||
| 4b43f027ce | |||
| 175bce6743 | |||
| 3380b0e201 | |||
| 619a2484ce | |||
| 24ac703d4d | |||
| c47eb5d718 | |||
| 3a345c3f4e | |||
| 86b304e173 | |||
| 01d02a9d71 | |||
| 2bf9f8bcb8 | |||
| 99a445b789 | |||
| ee39dda5b9 | |||
| be8430fbd9 | |||
| f1b00eb060 | |||
| d81afba93d | |||
| 223254eb07 | |||
| 698fa818e9 | |||
| 60d05e2b4d | |||
| 0ea994fb2f | |||
| eb370319cb | |||
| 9b25766fca | |||
| dfbeb24a33 | |||
| 3c4f106e14 | |||
| 4f5aee6830 | |||
| 42d64f93d8 | |||
| 6c11daccf8 | |||
| 88007dce34 | |||
| 1790f743d6 | |||
| ef6370f92e | |||
| 3cac617aff | |||
| e1550c8ffe | |||
| c60803ee2a | |||
| 9c5507f7b2 | |||
| 47e87c8363 | |||
| 652fb98d2c |
@@ -0,0 +1,44 @@
|
||||
# ── Docker build context — loại bỏ mã nguồn/bí mật không cần vào image ──
|
||||
.git
|
||||
.gitignore
|
||||
.github
|
||||
|
||||
# Mã nguồn client (JSX) — image CHỈ chứa bản precompiled (minified)
|
||||
app/static/js/app.jsx
|
||||
app/static/js/app.jsx.new
|
||||
*.jsx
|
||||
|
||||
# Bí mật
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Dependencies / build cache
|
||||
node_modules
|
||||
__pycache__
|
||||
*.pyc
|
||||
.cache
|
||||
.pytest_cache
|
||||
|
||||
# Tài liệu / kế hoạch / backup nội bộ
|
||||
wiki.md
|
||||
PLAN.md
|
||||
PLAN*
|
||||
plans/
|
||||
md/
|
||||
*.patch
|
||||
*.recovered
|
||||
index.html.recovered
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# Dữ liệu runtime (mount volume riêng — KHÔNG chép vào image)
|
||||
app/storage/uploads
|
||||
app/storage/processed
|
||||
app/storage/sonicforge.db
|
||||
celerybeat-schedule
|
||||
|
||||
# Test
|
||||
tests/
|
||||
tools/
|
||||
samples/
|
||||
@@ -0,0 +1,17 @@
|
||||
# ── SonicForge Studio — môi trường (SAO CHÉP sang .env — KHÔNG commit .env) ──
|
||||
|
||||
# Redis / Celery
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
CELERY_BROKER_URL=redis://redis:6379/0
|
||||
CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||
|
||||
# LLM (server-side proxy — client không thấy key)
|
||||
OPENAI_API_BASE=http://localhost:11434/v1
|
||||
OPENAI_API_KEY=ollama
|
||||
|
||||
# Auth
|
||||
SECRET_KEY=thay-bang-chuoi-ngau-nhien-dai
|
||||
DEFAULT_ADMIN_PASSWORD=thay-mat-khau-admin
|
||||
|
||||
# Storage (đường dẫn trong container)
|
||||
STORAGE_DIR=/app/app/storage
|
||||
@@ -0,0 +1,63 @@
|
||||
name: windows-build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install backend deps
|
||||
run: |
|
||||
pip install pyinstaller pywin32
|
||||
pip install -r requirements.txt
|
||||
# pedalboard --no-deps: tránh supertonic (kéo onnx2tf/opencv/pocket-tts —
|
||||
# xung đột numpy<2 vs >=2 — 2026-08-08)
|
||||
pip install --no-deps pedalboard>=0.8.0
|
||||
|
||||
- name: Precompile JS (Babel)
|
||||
shell: bash
|
||||
run: |
|
||||
npm install --no-audit --no-fund 2>/dev/null || true
|
||||
node node_modules/@babel/cli/bin/babel.js app/static/js/app.jsx --config-file ./babel.config.json -o app/static/js/app.precompiled.js
|
||||
|
||||
- name: Build daw_engine.exe (PyInstaller)
|
||||
run: pyinstaller engine.spec --clean --noconfirm
|
||||
|
||||
- name: Stage sidecar binary (Tauri naming)
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p src-tauri/binaries
|
||||
cp dist/daw_engine.exe "src-tauri/binaries/daw_engine-x86_64-pc-windows-msvc.exe"
|
||||
|
||||
- name: Install Tauri CLI
|
||||
run: npm install -D @tauri-apps/cli@^2
|
||||
|
||||
- name: Build Windows installers (NSIS + MSI)
|
||||
run: npx tauri build --no-bundle 2>/dev/null || npx tauri build
|
||||
|
||||
- name: Upload installer artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SonicForgeDAW-windows
|
||||
path: |
|
||||
src-tauri/target/release/bundle/nsis/*.exe
|
||||
src-tauri/target/release/bundle/msi/*.msi
|
||||
dist/daw_engine.exe
|
||||
@@ -0,0 +1,45 @@
|
||||
# BUILD WINDOWS — hướng dẫn build installer (.exe) trên máy Windows
|
||||
|
||||
## Yêu cầu trước khi build
|
||||
| Thành phần | Cài từ | Ghi chú |
|
||||
|---|---|---|
|
||||
| **Python 3.11+** | python.org | Tick **"Add to PATH"** |
|
||||
| **Node.js 20+** | nodejs.org | |
|
||||
| **Rust (MSVC toolchain)** | https://rustup.rs (hoặc script tự cài) | Cần cho Tauri |
|
||||
| **Visual Studio Build Tools** | visualstudio.microsoft.com | Workload **"Desktop development with C++"** (bắt buộc — MSVC linker cho Rust/Tauri) |
|
||||
| **WebView2 Runtime** | Windows 10/11 có sẵn; Windows 7/8 tải từ Microsoft | Tauri yêu cầu |
|
||||
|
||||
> Nếu lỗi `cargo: program not found` → chưa cài Rust: chạy `rustup-init.exe -y` rồi **mở cmd MỚI** (PATH refresh).
|
||||
|
||||
## Cách build — 1 lệnh
|
||||
```bat
|
||||
build_windows.bat
|
||||
```
|
||||
Script tự làm: cài Rust (nếu thiếu) → pip deps → Babel precompile → `pyinstaller engine.spec` (daw_engine.exe) → copy sidecar → `npx tauri build`.
|
||||
|
||||
## Kết quả
|
||||
```
|
||||
src-tauri\target\release\bundle\nsis\SonicForgeDAW_1.0.0_x64-setup.exe ← cài đặt (.exe)
|
||||
src-tauri\target\release\bundle\msi\SonicForgeDAW_1.0.0_x64_en-US.msi ← MSI
|
||||
dist\daw_engine.exe ← engine đơn (debug)
|
||||
```
|
||||
|
||||
## Thủ công (không dùng script)
|
||||
```bat
|
||||
pip install pyinstaller pywin32
|
||||
pip install -r requirements.txt
|
||||
pip install --no-deps pedalboard>=0.8.0
|
||||
node node_modules/@babel/cli/bin/babel.js app/static/js/app.jsx --config-file ./babel.config.json -o app/static/js/app.precompiled.js
|
||||
pyinstaller engine.spec --clean --noconfirm
|
||||
mkdir src-tauri\binaries
|
||||
copy dist\daw_engine.exe src-tauri\binaries\daw_engine-x86_64-pc-windows-msvc.exe
|
||||
npm install -D @tauri-apps/cli@^2
|
||||
npx tauri build
|
||||
```
|
||||
|
||||
## Kiểm thử sau cài (spec — mục IV)
|
||||
1. Mở app → Task Manager: `daw_engine.exe` chạy nền; đóng app → tiến trình tắt hẳn (không orphan).
|
||||
2. Browser ngoài mở `http://localhost:8000/docs` → Swagger hiển thị.
|
||||
3. Plugin Manager → path `C:\Program Files\Common Files\VST3` → Scan → tìm thấy VST3 (Vital, Surge XT...).
|
||||
4. Export WAV → file chứa tiếng VST3 (qua pedalboard).
|
||||
5. Dữ liệu lưu tại `%APPDATA%\SonicForgeDAW\`.
|
||||
@@ -0,0 +1,87 @@
|
||||
# DESKTOP INSTALL PLAN — SonicForge Studio (bản cài trực tiếp trên OS)
|
||||
|
||||
Mô hình: **server chạy nền trên máy người dùng (localhost), client mở bằng browser**.
|
||||
Không cần Electron — tận dụng stack hiện có (FastAPI + static JS precompiled).
|
||||
Soundfonts do **người dùng tự tải vào thư mục riêng** và **khai báo** cho ứng dụng.
|
||||
|
||||
---
|
||||
|
||||
## 1. KIẾN TRÚC
|
||||
|
||||
```
|
||||
┌─────────────────────────── NGƯỜI DÙNG (1 máy) ───────────────────────────┐
|
||||
│ Browser ──http://127.0.0.1:8000──▶ FastAPI (uvicorn, chạy NỀN) │
|
||||
│ │ │
|
||||
│ ~/SonicForgeStudio/ ├─ data/ (DB, uploads, cache) │
|
||||
│ ├─ data/sonicforge.db ├─ soundfonts/ (user tự bỏ .sf2) │
|
||||
│ ├─ soundfonts/*.sf2/.sf3 └─ quét nền 30s (scanner có sẵn) │
|
||||
│ └─ config.json ← khai báo folder soundfont │
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 2. ĐÓNG GÓI CÀI ĐẶT (per-OS) — server nền + browser client
|
||||
|
||||
### Thành phần đóng gói
|
||||
| Thành phần | Vai trò |
|
||||
|---|---|
|
||||
| Python backend (`app/`, `requirements.txt`) | Server — **PyInstaller `--onedir`** → binary (không cần Python trên máy user; bảo vệ source tốt hơn so với chạy .py) |
|
||||
| `app/static/` (precompiled.js + css + vendor) | Client — đóng vào package, server phục vụ |
|
||||
| VST3 plugins (`vst_plugins/`) | Copy theo platform (win .dll / mac .vst3 / linux .so) |
|
||||
| Service wrapper | Tự khởi động server nền + mở browser khi login |
|
||||
|
||||
### Cài đặt + service nền theo OS
|
||||
| OS | Installer | Service nền | Auto-open browser |
|
||||
|---|---|---|---|
|
||||
| **Windows** | Inno Setup (`.exe`) | Windows Service qua `NSSM`/`WinSW` hoặc Task Scheduler (logon) | `start http://127.0.0.1:8000` |
|
||||
| **macOS** | `.dmg` + `.app` (PyInstaller) | `launchd` LaunchAgent (`~/Library/LaunchAgents`) | `open http://127.0.0.1:8000` |
|
||||
| **Linux** | `.deb` / `.AppImage` | systemd **user** service (`~/.config/systemd/user/`) | `xdg-open http://127.0.0.1:8000` |
|
||||
|
||||
- Server bind **`127.0.0.1`** (không lộ mạng), port mặc định 8000 (config được).
|
||||
- App gồm 2 tiến trình nhỏ: `web` (uvicorn) + `worker` (celery) — hoặc gộp worker vào web ở chế độ desktop (đơn giản: `--pool=solo`, chạy celery trong tiến trình riêng nếu cần render nặng).
|
||||
|
||||
## 3. THƯ MỤC DỮ LIỆU NGƯỜI DÙNG
|
||||
```
|
||||
~/SonicForgeStudio/
|
||||
├── data/ # DB, uploads, processed (thay app/storage khi chạy desktop)
|
||||
├── soundfonts/ # USER tự tải .sf2/.sf3 vào đây (mặc định được quét)
|
||||
└── config.json # cấu hình: soundfont_dirs, port, autostart...
|
||||
```
|
||||
- `config.py`: thêm `DATA_DIR` (env `SFDATA_DIR`, mặc định `~/SonicForgeStudio/data`), `SOUNDFONT_DIRS`.
|
||||
|
||||
## 4. SOUNDFONT DO NGƯỜI DÙNG QUẢN LÝ (tải + khai báo)
|
||||
**Cơ chế hiện có (tận dụng):** `app/core/soundfont_scanner.py` — `SoundFontAutoScanner` quét nền 30s
|
||||
`/opt/daw_engine/soundfonts` + `storage/soundfonts` → catalog → API list qua `app/api/v1/plugins.py`.
|
||||
|
||||
**Việc cần làm (mở rộng):**
|
||||
1. **Scanner nhận folder người dùng:** constructor nhận thêm `user_dirs` (từ `SOUNDFONT_DIRS` env + `config.json`) — merge vào catalog (ưu tiên: user > system).
|
||||
2. **Khai báo folder — 2 cách:**
|
||||
- **UI** (chính): Settings → "Soundfonts" → nút **Add folder…** (chọn thư mục chứa .sf2) → lưu vào `config.json` → gọi scanner rescan.
|
||||
- **config.json** (thủ công): `{ "soundfont_dirs": ["D:/SF", "/Users/me/sf"] }`.
|
||||
3. **API:** thêm endpoint `POST /api/v1/plugins/soundfonts/dirs` (đăng ký folder) + `GET .../dirs` (liệt kê) — scanner reload.
|
||||
4. **UI danh sách:** hiển thị catalog (tên SF, kích thước, folder nguồn) — chọn = load vào FluidSynth (luồng có sẵn qua `SonicSF.selectInstrument`).
|
||||
5. **Số hóa:** không upload file — server đọc trực tiếp từ đường dẫn user khai báo (không nhân đôi dữ liệu).
|
||||
|
||||
## 5. BUILD + PHÁT HÀNH
|
||||
```
|
||||
code → build.mjs (precompiled + ?v=) → PyInstaller (server binary) → đóng installer theo OS
|
||||
→ ký số (tùy chọn) → phát hành (GitHub Releases / trang riêng)
|
||||
```
|
||||
- **GitHub Actions matrix** (windows-latest / macos-latest / ubuntu-latest): test → build → installer artifact.
|
||||
- Installer gồm: binary server, static/, VST plugins nền tảng, script tạo service + mở browser, mặc định tạo `~/SonicForgeStudio/` lần chạy đầu.
|
||||
|
||||
## 6. CẬP NHẬT
|
||||
- **Version check**: khi mở app, gọi endpoint version (file `version.json` đóng kèm + so sánh remote) → thông báo bản mới + link tải installer.
|
||||
- Cập nhật = chạy installer mới (ghi đè, GIỮ NGUYÊN `~/SonicForgeStudio/` — data + soundfonts không đụng).
|
||||
- `?v=` cache-bust JS mỗi bản (cơ chế đã có) — browser không dính cache cũ.
|
||||
|
||||
## 7. CHECKLIST CODE CẦN LÀM
|
||||
- [ ] `config.py`: `DATA_DIR`, `SOUNDFONT_DIRS` (env + config.json)
|
||||
- [ ] `soundfont_scanner.py`: nhận `user_dirs`, merge catalog, ưu tiên user
|
||||
- [ ] API: `POST/GET .../soundfonts/dirs` (đăng ký/liệt kê folder) + rescan
|
||||
- [ ] UI Settings → Soundfonts (Add folder, Browse, list, load)
|
||||
- [ ] `soundfontStorage.js`: chuyển hướng sang catalog folder-scan (giữ upload path cho dự án cũ)
|
||||
- [ ] PyInstaller spec: bundle static/ + vendor (libfluidsynth wasm, vst), bind 127.0.0.1
|
||||
- [ ] Service wrappers: Windows (NSSM/WinSW), macOS (launchd), Linux (systemd user) + auto-open browser
|
||||
- [ ] Installer scripts: Inno Setup (.exe), dmg (macOS), deb/AppImage (Linux)
|
||||
- [ ] CI matrix build 3 OS + release artifacts
|
||||
- [ ] `version.json` + in-app update check
|
||||
@@ -0,0 +1,85 @@
|
||||
# DISTRIBUTION PLAN — SonicForge Studio (Alpha → Beta → Production)
|
||||
|
||||
Mục tiêu: phân phối ứng dụng cho người dùng **không chia sẻ mã nguồn**, có pipeline
|
||||
cập nhật liên tục. Áp dụng cho backend Python/FastAPI + frontend JS (Babel precompiled).
|
||||
|
||||
---
|
||||
|
||||
## 1. TÁCH BÍ MẬT — FILE .env
|
||||
|
||||
| Biến | Dùng cho | Bắt buộc |
|
||||
|---|---|---|
|
||||
| `REDIS_URL` | Redis/Celery broker | ✓ |
|
||||
| `CELERY_BROKER_URL` / `CELERY_RESULT_BACKEND` | Celery | ✓ |
|
||||
| `OPENAI_API_BASE` / `OPENAI_API_KEY` | LLM server-side (client KHÔNG thấy key — `aiGateway.js` chỉ gọi proxy server) | ✓ |
|
||||
| `SECRET_KEY` | Auth token (app/core/auth.py) | ✓ (mặc định trống → phải set) |
|
||||
| `DEFAULT_ADMIN_PASSWORD` | Admin mặc định | thay đổi ở production |
|
||||
|
||||
- File `.env.example` đã tạo (commit được). `.env` thật KHÔNG commit (đã vào `.dockerignore`).
|
||||
- **Nguyên tắc:** client JS không bao giờ chứa secret — mọi API key nằm server (env) hoặc proxy.
|
||||
|
||||
## 2. BUILD IMAGE — KHÔNG CHIA SẺ MÃ NGUỒN
|
||||
|
||||
### 2.1 Chặn source khỏi image (`.dockerignore` — đã tạo)
|
||||
Loại khỏi build context: `.git`, `app/static/js/app.jsx` (source JSX), `wiki.md`, `plans/`, `*.patch`, `.env`, `tests/`, `node_modules`, backup...
|
||||
|
||||
### 2.2 Frontend — chỉ ship bản precompiled
|
||||
- Build JS: `node build.mjs` (Babel → `app.precompiled.js` minified) — **CHỈ bản minified vào image**, source `.jsx` bị `.dockerignore` chặn.
|
||||
- Bump `?v=` (cache-bust) mỗi bản phát hành — user cập nhật không dính cache cũ.
|
||||
|
||||
### 2.3 Backend Python — giới hạn đọc source
|
||||
- Python không biên dịch native mặc định → `.py` vẫn đọc được trong image. 3 lớp bảo vệ:
|
||||
1. **Registry riêng tư** (GHCR / Docker Hub private / Harbor) — image KHÔNG public — người dùng chỉ nhận qua pull có auth.
|
||||
2. **Secret chỉ qua env** — không hardcode gì trong image.
|
||||
3. *(Tùy chọn, giai đoạn sau)* compile Python bằng **Nuitka** → `.so` cho `app/core`, `app/api` (giữ `main.py`/`config.py` đọc được — không chứa bí mật).
|
||||
|
||||
### 2.4 Cách build + push
|
||||
```bash
|
||||
# 1. Precompile JS + bump ?v=
|
||||
node build.mjs # hoặc: NODE=... babel ... (xem wiki)
|
||||
# 2. Build image (tag theo version)
|
||||
docker build -t ghcr.io/<org>/sonicforge-studio:v1.0.0-alpha.1 .
|
||||
docker push ghcr.io/<org>/sonicforge-studio:v1.0.0-alpha.1
|
||||
# 3. Production chạy image đã push (KHÔNG mount source):
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
- Compose production (`docker-compose.prod.yml` — đã tạo): image từ registry, `env_file: .env`,
|
||||
volumes bền (uploads/processed/db), asset dirs qua env (`VST3_DIR`, `SOUNDFONTS_DIR`, `PIANOBOOK_DIR`),
|
||||
KHÔNG có `.:/app` (bỏ mount dev của compose cũ).
|
||||
|
||||
## 3. KẾ HOẠCH PHÁT HÀNH
|
||||
|
||||
### Giai đoạn 1 — ALPHA (nội bộ dev/QA)
|
||||
- Tag: `v0.x.x-alpha.N` — chạy `docker compose up --build` (dev mount OK).
|
||||
- Log verbose; chưa quan tâm bảo mật; test tính năng + thu hồi nhanh.
|
||||
- Mỗi thay đổi: bump `?v=` → build → tag → ghi wiki.md.
|
||||
|
||||
### Giai đoạn 2 — BETA (nhóm người dùng mời)
|
||||
- Tag: `v0.x.x-beta.N` — image push **registry riêng tư**.
|
||||
- Người dùng: `docker compose -f docker-compose.prod.yml pull && up -d` (chỉ cần `.env` + image).
|
||||
- Thu log lỗi: thêm endpoint `/health` + (tùy chọn) Sentry/self-host error tracking.
|
||||
- **Feature flags** (biến env `FEATURE_*`) để tắt tính năng rủi ro từ xa.
|
||||
|
||||
### Giai đoạn 3 — PRODUCTION
|
||||
- Tag: `vX.Y.Z` (semver) + `latest`.
|
||||
- **Trước khi release:** backup volumes (`docker run --rm -v sf_db:/data -v $PWD:/backup alpine tar czf /backup/db-<ver>.tgz /data`), migrate dữ liệu nếu schema đổi.
|
||||
- Triển khai: pull image mới → recreate (rolling — web trước, worker sau) → healthcheck.
|
||||
- `SECRET_KEY`, `DEFAULT_ADMIN_PASSWORD`, API keys: quản lý qua secret manager (Docker secrets / vault) — không ở compose file.
|
||||
|
||||
## 4. PIPELINE CẬP NHẬT (lặp lại mỗi release)
|
||||
```
|
||||
code mới → build.mjs (precompiled + ?v=) → docker build (tag mới)
|
||||
→ push registry → [beta/prod] pull + recreate → backup trước nếu prod → kiểm tra /health
|
||||
```
|
||||
- Phiên bản: `git tag vX.Y.Z` — build tag tự động từ git (`git describe --tags`).
|
||||
- Rollback: giữ tag cũ — `docker compose -f docker-compose.prod.yml up -d` với `IMAGE=...:<tag cũ>`.
|
||||
|
||||
## 5. VIỆC CẦN LÀM (checklist)
|
||||
- [x] `.env.example` — liệt kê đủ 7 biến
|
||||
- [x] `.dockerignore` — chặn source/secret/docs
|
||||
- [x] `docker-compose.prod.yml` — production (registry + env_file + volumes, bỏ mount source)
|
||||
- [ ] Kiểm tra endpoint `/health` (tạo nếu chưa có — healthcheck compose đang trỏ tới)
|
||||
- [ ] Chọn registry (GHCR/Docker Hub private/Harbor) + tạo token CI
|
||||
- [ ] Nuitka compile `app/core`+`app/api` (tùy chọn — nếu cần bảo vệ backend chặt hơn)
|
||||
- [ ] CI (GitHub Actions): test → build → push tag → deploy beta tự động
|
||||
- [ ] Backup script volumes trước mỗi production release
|
||||
+27
-22
@@ -2,28 +2,33 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Cài đặt các gói thư viện đồ hoạ và asound bắt buộc đối với JUCE / VST3 Linux (22_CLIENT_DESK.md)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libgl1 \
|
||||
libglx-mesa0 \
|
||||
libglu1-mesa \
|
||||
libasound2 \
|
||||
libjack-jackd2-0 \
|
||||
libfreetype6 \
|
||||
libfontconfig1 \
|
||||
libx11-6 \
|
||||
libxext6 \
|
||||
libxinerama1 \
|
||||
libxrandr2 \
|
||||
libxcursor1 \
|
||||
xvfb \
|
||||
ffmpeg \
|
||||
libsndfile1 \
|
||||
libfluidsynth3 \
|
||||
libcurl4 \
|
||||
build-essential \
|
||||
fluidsynth \
|
||||
vorbis-tools \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# ⚠️ Check trước: package đã có (layer cache bị invalidation) → BỎ QUA apt-get (tiết kiệm thời gian build)
|
||||
RUN if command -v fluidsynth >/dev/null 2>&1 && command -v ffmpeg >/dev/null 2>&1 && command -v Xvfb >/dev/null 2>&1; then \
|
||||
echo "Apt packages already installed - skip apt-get update/install"; \
|
||||
else \
|
||||
apt-get update && apt-get install -y \
|
||||
libgl1 \
|
||||
libglx-mesa0 \
|
||||
libglu1-mesa \
|
||||
libasound2 \
|
||||
libjack-jackd2-0 \
|
||||
libfreetype6 \
|
||||
libfontconfig1 \
|
||||
libx11-6 \
|
||||
libxext6 \
|
||||
libxinerama1 \
|
||||
libxrandr2 \
|
||||
libxcursor1 \
|
||||
xvfb \
|
||||
ffmpeg \
|
||||
libsndfile1 \
|
||||
libfluidsynth3 \
|
||||
libcurl4 \
|
||||
build-essential \
|
||||
fluidsynth \
|
||||
vorbis-tools \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# Thiết lập biến môi trường hiển thị cho X11 ảo
|
||||
ENV DISPLAY=:99
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 SonicForge Studio contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## Third-party components
|
||||
|
||||
Code viết riêng của dự án (app/static/js/services/*, components/*, app.jsx)
|
||||
thuộc license MIT trên.
|
||||
|
||||
Các thành phần bên thứ ba giữ license gốc:
|
||||
|
||||
- **React / react-dom** — MIT
|
||||
- **Babel** (@babel/cli, core, preset-react) — MIT
|
||||
- **jsdom** — MIT
|
||||
- **Tailwind CSS** (app/static/css/tailwind.min.css) — MIT
|
||||
- **Lucide icons** — ISC
|
||||
- **FluidSynth 2.3.0** (app/static/js/vendor/libfluidsynth-2.3.0-sf3.js/.wasm) —
|
||||
GNU LGPL v2.1+ — xem https://www.gnu.org/licenses/lgpl-2.1.html
|
||||
|
||||
Chi tiết + attribution từng thành phần: xem THIRD_PARTY_LICENSES.md.
|
||||
@@ -0,0 +1,37 @@
|
||||
# THIRD-PARTY LICENSES — SonicForgeStudio
|
||||
|
||||
Kiểm tra ngày 2026-08-07. Danh sách thành phần bên thứ ba + license.
|
||||
|
||||
## 1. Dependencies (npm — package.json)
|
||||
| Thành phần | License | Ghi chú |
|
||||
|---|---|---|
|
||||
| react / react-dom 19.x | MIT | OK |
|
||||
| @babel/cli, @babel/core, @babel/preset-react | MIT | Chỉ build-time |
|
||||
| jsdom | MIT | Chỉ build-time/test |
|
||||
|
||||
## 2. Thư viện nhúng (app/static)
|
||||
| File | Nguồn | License | Trạng thái |
|
||||
|---|---|---|---|
|
||||
| `css/tailwind.min.css` | Tailwind CSS | MIT | OK — nên giữ attribution |
|
||||
| `js/services/spessasynth_processor.min.js` | SpessaSynth (KHÔNG còn dùng — không được include trong index.html — chỉ FluidSynth) | MIT | ✅ KHÔNG có code nào gọi (chỉ còn comment cũ app.jsx:14276 + file chết) — có thể xóa file |
|
||||
| `js/vendor/libfluidsynth-2.3.0-sf3.js` + `.wasm` | FluidSynth 2.3.0 (Emscripten) | **LGPL-2.1+** | ⚠️ Bắt buộc giữ license notice + attribution + (nếu phân phối bản build) cung cấp link/tài liệu LGPL |
|
||||
| `js/services/fluidsynthLoader.js`, `worklets/fluidsynth-bridge.js` | Bản tự viết (bọc FluidSynth) | Dự án | OK |
|
||||
| Lucide icons (inline `data-lucide`) | Lucide | ISC (MIT-compatible) | OK |
|
||||
|
||||
## 3. Nội dung âm thanh (app/storage)
|
||||
| File | Nguồn | License | Trạng thái |
|
||||
|---|---|---|---|
|
||||
| `soundfonts/518e850f-...sf2` | "General MIDI SoundFont v3.0" — © 2006-2010 Rich "Weeds" Nagel — "Some rights reserved" | Không có text đầy đủ trong file (chỉ ICOP ngắn). Tuyên bố: "created from various **commercial**, custom, and freeware soundfonts and samples" | ⚠️ RỦI RO: (a) điều kiện "some rights reserved" không rõ ràng (thường là CC-BY — cần ghi attribution; có thể hạn chế thương mại); (b) samples gốc có nguồn commercial — quyền tái phân phối phụ thuộc tuyên bố tác giả. **Khuyến nghị: thay bằng SF2 license rõ (FluidR3_GM — MIT/GPL-2; GeneralUser GS — CC-BY-SA; Arachno — public domain) HOẶC giữ + ghi attribution đầy đủ.** |
|
||||
| `uploads/user_anonymous_*.mp3` | File người dùng upload | Thuộc người dùng | OK — không phải thành phần phân phối |
|
||||
| SGM-V2.01 (xuất hiện trong log `sfId: SGM-V2.01`) | SGM-V2.01 | Freeware — tác giả cho phép dùng nhưng **hạn chế redistribution** (cần permission) | ⚠️ Nếu vẫn dùng để phân phối bản build — cần permission từ tác giả; không nhúng vào sản phẩm |
|
||||
|
||||
## 4. Assets
|
||||
- `templates/favicon.svg`, `images/SonicForgeUI.png` — nội bộ (tự tạo) — OK.
|
||||
- Code trong `app/static/js/services/*`, `app/static/js/components/*`, `app.jsx` — tự viết — thuộc dự án.
|
||||
|
||||
## KẾT LUẬN
|
||||
- **Không phát hiện vi phạm bản quyền rõ ràng** (không có code GPL bị nhúng vào project MIT; LGPL của FluidSynth tương thích nếu giữ notice).
|
||||
- **3 điểm cần xử lý trước khi phân phối công khai:**
|
||||
1. Bổ sung `LICENSE` text (MIT + LGPL-2.1) + attribution cho SpessaSynth (MIT notice) và Tailwind.
|
||||
2. Ghi attribution SF2 Rich Nagel ("General MIDI SoundFont v3.0 — © 2006-2010 Rich 'Weeds' Nagel — Some rights reserved") hoặc thay soundfont khác license rõ.
|
||||
3. Không nhúng SGM-V2.01 vào bản phân phối (hạn chế redistribution).
|
||||
+112
-10
@@ -1,7 +1,16 @@
|
||||
import asyncio
|
||||
import ipaddress
|
||||
import json
|
||||
import socket
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, Any, Dict, List
|
||||
from typing import Any, Dict
|
||||
|
||||
from app.api.v1.auth import get_current_user
|
||||
from app.api.v1.user_config import _load_ai_configs, _get_default_providers
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -10,17 +19,108 @@ class ProxyRequest(BaseModel):
|
||||
headers: Dict[str, str] = {}
|
||||
body: Dict[str, Any] = {}
|
||||
|
||||
import json
|
||||
|
||||
# Ranges that are never legitimate AI endpoints: cloud metadata + this host.
|
||||
_BLOCKED_NETWORKS = [
|
||||
ipaddress.ip_network("169.254.0.0/16"), # link-local / cloud metadata
|
||||
ipaddress.ip_network("0.0.0.0/8"),
|
||||
]
|
||||
# Private ranges: only reachable when the target host is one the user has
|
||||
# explicitly configured as an AI provider (e.g. local Ollama/LM Studio).
|
||||
_PRIVATE_NETWORKS = [
|
||||
ipaddress.ip_network("10.0.0.0/8"),
|
||||
ipaddress.ip_network("172.16.0.0/12"),
|
||||
ipaddress.ip_network("192.168.0.0/16"),
|
||||
ipaddress.ip_network("127.0.0.0/8"),
|
||||
ipaddress.ip_network("::1/128"),
|
||||
ipaddress.ip_network("fc00::/7"), # ULA
|
||||
]
|
||||
|
||||
_LOOPBACK_HOSTS = {"localhost", "127.0.0.1", "::1", "0.0.0.0"}
|
||||
|
||||
|
||||
def _configured_ai_hosts(user_id: str) -> set:
|
||||
"""Hosts the user has configured as AI providers (from saved config + defaults)."""
|
||||
hosts = set()
|
||||
configs = _load_ai_configs()
|
||||
providers = configs.get(user_id) or _get_default_providers()
|
||||
for p in providers:
|
||||
base = (p.get("api_base_url") or "").strip()
|
||||
if not base:
|
||||
continue
|
||||
try:
|
||||
host = urlparse(base).hostname
|
||||
if host:
|
||||
hosts.add(host.lower())
|
||||
except Exception:
|
||||
continue
|
||||
return hosts
|
||||
|
||||
|
||||
async def _resolve_host_ips(hostname: str):
|
||||
"""Resolve hostname to IPs (non-blocking). Returns list of ipaddress objects."""
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
infos = await loop.run_in_executor(None, socket.getaddrinfo, hostname, None)
|
||||
ips = []
|
||||
for info in infos:
|
||||
try:
|
||||
ips.append(ipaddress.ip_address(info[4][0]))
|
||||
except ValueError:
|
||||
continue
|
||||
return ips
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
async def _validate_target_url(url: str, user_id: str):
|
||||
parsed = urlparse(url)
|
||||
if parsed.scheme not in ("http", "https"):
|
||||
raise HTTPException(status_code=400, detail="URL chỉ hỗ trợ giao thức http/https")
|
||||
if parsed.username or parsed.password:
|
||||
raise HTTPException(status_code=400, detail="URL không được chứa thông tin đăng nhập")
|
||||
hostname = (parsed.hostname or "").lower()
|
||||
if not hostname:
|
||||
raise HTTPException(status_code=400, detail="URL không hợp lệ")
|
||||
|
||||
allowed_hosts = _configured_ai_hosts(user_id)
|
||||
|
||||
# Hostname-level fast path for loopback hosts
|
||||
if hostname in _LOOPBACK_HOSTS:
|
||||
if hostname in allowed_hosts:
|
||||
return
|
||||
raise HTTPException(status_code=403, detail="Target nội bộ không nằm trong danh sách AI provider đã cấu hình")
|
||||
|
||||
# Try direct IP parse (hostname may itself be an IP)
|
||||
try:
|
||||
ip = ipaddress.ip_address(hostname)
|
||||
ips = [ip]
|
||||
except ValueError:
|
||||
ips = await _resolve_host_ips(hostname)
|
||||
|
||||
if not ips:
|
||||
raise HTTPException(status_code=502, detail="Không phân giải được hostname")
|
||||
|
||||
for ip in ips:
|
||||
if any(ip in net for net in _BLOCKED_NETWORKS):
|
||||
raise HTTPException(status_code=403, detail="Target bị chặn (metadata/link-local không được phép)")
|
||||
if any(ip in net for net in _PRIVATE_NETWORKS):
|
||||
if hostname in allowed_hosts:
|
||||
continue
|
||||
raise HTTPException(status_code=403, detail="Target IP nội bộ không nằm trong danh sách AI provider đã cấu hình")
|
||||
|
||||
|
||||
@router.post("/proxy")
|
||||
async def proxy_llm(req: ProxyRequest):
|
||||
async def proxy_llm(req: ProxyRequest, current_user: dict = Depends(get_current_user)):
|
||||
await _validate_target_url(req.url, current_user["user_id"])
|
||||
# Never forward the app's own auth token upstream.
|
||||
headers = {
|
||||
k: v for k, v in req.headers.items()
|
||||
if k.lower() not in ("host", "origin", "referer", "x-auth-token")
|
||||
}
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=180.0) as client:
|
||||
resp = await client.post(
|
||||
req.url,
|
||||
headers={k: v for k, v in req.headers.items() if k.lower() not in ('host', 'origin', 'referer')},
|
||||
json=req.body
|
||||
)
|
||||
async with httpx.AsyncClient(timeout=180.0, follow_redirects=False) as client:
|
||||
resp = await client.post(req.url, headers=headers, json=req.body)
|
||||
raw = resp.text
|
||||
try:
|
||||
return resp.json()
|
||||
@@ -36,5 +136,7 @@ async def proxy_llm(req: ProxyRequest):
|
||||
if 'localhost' in req.url or '127.0.0.1' in req.url:
|
||||
msg += "\nNếu app chạy trong Docker, localhost trỏ vào container, không ra host.\nHãy thay localhost bằng host.docker.internal hoặc IP bridge Docker (172.17.0.1)."
|
||||
raise HTTPException(status_code=502, detail=msg)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
+69
-73
@@ -8,12 +8,32 @@ from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
import json
|
||||
from app.config import settings
|
||||
from app.api.v1.auth import get_current_user
|
||||
from app.api.v1.auth import get_current_user, enforce_password_changed
|
||||
from app.api.v1.projects import get_optional_user
|
||||
from app.models.user import get_db_connection
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
MAX_AUDIO_UPLOAD_BYTES = 1024 * 1024 * 1024 # 1 GB
|
||||
|
||||
def _safe_file_id(file_id: str) -> str:
|
||||
"""Strip any path components from a client-supplied file id."""
|
||||
if not file_id:
|
||||
return ""
|
||||
return os.path.basename(file_id.replace("\\", "/"))
|
||||
|
||||
def _resolve_storage_path(file_id: str) -> str:
|
||||
"""Return the existing file path (processed first, then uploads) for a
|
||||
sanitized file id, or '' when not found."""
|
||||
fid = _safe_file_id(file_id)
|
||||
if not fid:
|
||||
return ""
|
||||
for d in (settings.PROCESSED_DIR, settings.UPLOADS_DIR):
|
||||
p = os.path.join(d, fid)
|
||||
if os.path.isfile(p):
|
||||
return p
|
||||
return ""
|
||||
|
||||
class EditRequest(BaseModel):
|
||||
file_id: str
|
||||
cut_start_ms: Optional[float] = None
|
||||
@@ -58,16 +78,32 @@ class PythonToolRequest(BaseModel):
|
||||
|
||||
@router.post("/upload")
|
||||
async def upload_audio(file: UploadFile = File(...), current_user: Optional[dict] = Depends(get_optional_user)):
|
||||
if current_user:
|
||||
enforce_password_changed(current_user)
|
||||
user_id = current_user["user_id"] if current_user else "anonymous"
|
||||
ext = os.path.splitext(file.filename)[1]
|
||||
ext = os.path.splitext(file.filename or "")[1]
|
||||
if not ext:
|
||||
ext = ".wav"
|
||||
file_id = f"user_{user_id}_{uuid.uuid4()}{ext}"
|
||||
file_path = os.path.join(settings.UPLOADS_DIR, file_id)
|
||||
|
||||
# Stream upload in chunks with a hard size cap (avoids loading a multi-GB
|
||||
# WAV into RAM and bounds disk usage).
|
||||
with open(file_path, "wb") as f:
|
||||
content = await file.read()
|
||||
f.write(content)
|
||||
size = 0
|
||||
while True:
|
||||
chunk = await file.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
size += len(chunk)
|
||||
if size > MAX_AUDIO_UPLOAD_BYTES:
|
||||
f.close()
|
||||
try:
|
||||
os.remove(file_path)
|
||||
except OSError:
|
||||
pass
|
||||
raise HTTPException(status_code=413, detail="File âm thanh quá lớn (giới hạn 1GB)")
|
||||
f.write(chunk)
|
||||
|
||||
# Save original filename as sidecar metadata
|
||||
import json
|
||||
@@ -90,15 +126,12 @@ async def upload_audio(file: UploadFile = File(...), current_user: Optional[dict
|
||||
|
||||
@router.post("/edit")
|
||||
async def edit_audio(req: EditRequest):
|
||||
upload_path = os.path.join(settings.UPLOADS_DIR, req.file_id)
|
||||
processed_path = os.path.join(settings.PROCESSED_DIR, req.file_id)
|
||||
|
||||
# Use uploaded file if it exists, or look in processed if it was already edited
|
||||
if not os.path.exists(upload_path) and not os.path.exists(processed_path):
|
||||
if not _resolve_storage_path(req.file_id):
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
from app.tasks.worker import edit_audio_task
|
||||
task = edit_audio_task.delay(req.dict())
|
||||
task = edit_audio_task.delay(req.model_dump())
|
||||
|
||||
return {
|
||||
"task_id": task.id
|
||||
@@ -106,15 +139,10 @@ async def edit_audio(req: EditRequest):
|
||||
|
||||
@router.get("/download/{file_id}")
|
||||
async def download_audio(file_id: str):
|
||||
processed_path = os.path.join(settings.PROCESSED_DIR, file_id)
|
||||
upload_path = os.path.join(settings.UPLOADS_DIR, file_id)
|
||||
|
||||
if os.path.exists(processed_path):
|
||||
return FileResponse(processed_path, media_type="audio/wav", filename=file_id)
|
||||
elif os.path.exists(upload_path):
|
||||
return FileResponse(upload_path, media_type="audio/wav", filename=file_id)
|
||||
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
path = _resolve_storage_path(file_id)
|
||||
if not path:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
return FileResponse(path, media_type="audio/wav", filename=os.path.basename(path))
|
||||
|
||||
@router.get("/waveform/{file_id}")
|
||||
async def get_waveform(file_id: str, num_peaks: int = Query(default=800, ge=50, le=4000)):
|
||||
@@ -122,14 +150,8 @@ async def get_waveform(file_id: str, num_peaks: int = Query(default=800, ge=50,
|
||||
API endpoint vẽ Peak Waveform đồng bộ (Week 2).
|
||||
Trả về dữ liệu peak waveform cho hiển thị đồ thị sóng âm trên Frontend.
|
||||
"""
|
||||
upload_path = os.path.join(settings.UPLOADS_DIR, file_id)
|
||||
processed_path = os.path.join(settings.PROCESSED_DIR, file_id)
|
||||
|
||||
if os.path.exists(processed_path):
|
||||
file_path = processed_path
|
||||
elif os.path.exists(upload_path):
|
||||
file_path = upload_path
|
||||
else:
|
||||
file_path = _resolve_storage_path(file_id)
|
||||
if not file_path:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
from app.core.dsp_utils import generate_peak_waveform
|
||||
@@ -140,14 +162,8 @@ async def get_waveform_rms(file_id: str, num_points: int = Query(default=800, ge
|
||||
"""
|
||||
API endpoint vẽ RMS Waveform (mượt hơn peak).
|
||||
"""
|
||||
upload_path = os.path.join(settings.UPLOADS_DIR, file_id)
|
||||
processed_path = os.path.join(settings.PROCESSED_DIR, file_id)
|
||||
|
||||
if os.path.exists(processed_path):
|
||||
file_path = processed_path
|
||||
elif os.path.exists(upload_path):
|
||||
file_path = upload_path
|
||||
else:
|
||||
file_path = _resolve_storage_path(file_id)
|
||||
if not file_path:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
from app.core.dsp_utils import generate_rms_waveform
|
||||
@@ -159,26 +175,20 @@ async def analyze_audio_with_ai(req: AIAnalysisRequest):
|
||||
API endpoint phân tích cấu trúc khuôn nhạc bằng AI (Week 4).
|
||||
Gọi OpenAI Compatible API (DeepSeek/Ollama) để phân đoạn bố cục.
|
||||
"""
|
||||
upload_path = os.path.join(settings.UPLOADS_DIR, req.file_id)
|
||||
processed_path = os.path.join(settings.PROCESSED_DIR, req.file_id)
|
||||
|
||||
if os.path.exists(processed_path):
|
||||
file_path = processed_path
|
||||
elif os.path.exists(upload_path):
|
||||
file_path = upload_path
|
||||
else:
|
||||
file_path = _resolve_storage_path(req.file_id)
|
||||
if not file_path:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
from app.tasks.worker import analyze_ai_task
|
||||
task = analyze_ai_task.delay(
|
||||
file_id=req.file_id,
|
||||
file_id=_safe_file_id(req.file_id),
|
||||
api_base_url=req.api_base_url,
|
||||
model=req.model
|
||||
)
|
||||
|
||||
return {
|
||||
"task_id": task.id,
|
||||
"file_id": req.file_id
|
||||
"file_id": _safe_file_id(req.file_id)
|
||||
}
|
||||
|
||||
@router.post("/export")
|
||||
@@ -186,19 +196,13 @@ async def export_audio(req: ExportRequest):
|
||||
"""
|
||||
API endpoint xuất tệp âm thanh sang nhiều định dạng (WAV/MP3/OGG).
|
||||
"""
|
||||
upload_path = os.path.join(settings.UPLOADS_DIR, req.file_id)
|
||||
processed_path = os.path.join(settings.PROCESSED_DIR, req.file_id)
|
||||
|
||||
if os.path.exists(processed_path):
|
||||
source_path = processed_path
|
||||
elif os.path.exists(upload_path):
|
||||
source_path = upload_path
|
||||
else:
|
||||
source_path = _resolve_storage_path(req.file_id)
|
||||
if not source_path:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
from app.tasks.worker import export_audio_task
|
||||
task = export_audio_task.delay(
|
||||
file_id=req.file_id,
|
||||
file_id=_safe_file_id(req.file_id),
|
||||
format=req.format,
|
||||
sample_rate=req.sample_rate,
|
||||
bit_depth=req.bit_depth
|
||||
@@ -206,29 +210,24 @@ async def export_audio(req: ExportRequest):
|
||||
|
||||
return {
|
||||
"task_id": task.id,
|
||||
"file_id": req.file_id
|
||||
"file_id": _safe_file_id(req.file_id)
|
||||
}
|
||||
|
||||
@router.post("/ai-scan")
|
||||
async def ai_scan_audio(req: AIScanRequest):
|
||||
async def ai_scan_audio(req: AIScanRequest, current_user: Optional[dict] = Depends(get_optional_user)):
|
||||
"""
|
||||
17_AI_SCAN.md Feature 1: AI Loop Scan & Automated Marker Labeling.
|
||||
Uses AIDSPEngine to find optimal recurring loop region with zero-crossing alignment.
|
||||
"""
|
||||
if current_user:
|
||||
enforce_password_changed(current_user)
|
||||
from app.core.ai_dsp_engine import AIDSPEngine
|
||||
import soundfile as sf
|
||||
import numpy as np
|
||||
|
||||
file_path = None
|
||||
if req.file_id:
|
||||
upload_path = os.path.join(settings.UPLOADS_DIR, req.file_id)
|
||||
processed_path = os.path.join(settings.PROCESSED_DIR, req.file_id)
|
||||
if os.path.exists(processed_path):
|
||||
file_path = processed_path
|
||||
elif os.path.exists(upload_path):
|
||||
file_path = upload_path
|
||||
file_path = _resolve_storage_path(req.file_id) if req.file_id else ""
|
||||
|
||||
if file_path and os.path.exists(file_path):
|
||||
if file_path:
|
||||
data, sr = sf.read(file_path)
|
||||
if data.ndim > 1:
|
||||
data = data.T
|
||||
@@ -252,6 +251,8 @@ async def ai_cut_audio(req: AICutRequest, current_user: Optional[dict] = Depends
|
||||
Executes raw binary sample slice at exact zero-crossing coordinates.
|
||||
"""
|
||||
user_id = current_user["user_id"] if current_user else "anonymous"
|
||||
if current_user:
|
||||
enforce_password_changed(current_user)
|
||||
from app.core.ai_dsp_engine import AIDSPEngine
|
||||
import soundfile as sf
|
||||
import numpy as np
|
||||
@@ -259,16 +260,9 @@ async def ai_cut_audio(req: AICutRequest, current_user: Optional[dict] = Depends
|
||||
output_file_id = f"user_{user_id}_ai_cut_{uuid.uuid4().hex[:8]}.wav"
|
||||
out_path = os.path.join(settings.PROCESSED_DIR, output_file_id)
|
||||
|
||||
file_path = None
|
||||
if req.file_id:
|
||||
upload_path = os.path.join(settings.UPLOADS_DIR, req.file_id)
|
||||
processed_path = os.path.join(settings.PROCESSED_DIR, req.file_id)
|
||||
if os.path.exists(processed_path):
|
||||
file_path = processed_path
|
||||
elif os.path.exists(upload_path):
|
||||
file_path = upload_path
|
||||
file_path = _resolve_storage_path(req.file_id) if req.file_id else ""
|
||||
|
||||
if file_path and os.path.exists(file_path):
|
||||
if file_path:
|
||||
data, sr = sf.read(file_path)
|
||||
if data.ndim > 1:
|
||||
data = data.T
|
||||
@@ -295,6 +289,8 @@ async def run_python_dsp_tool(req: PythonToolRequest, current_user: Optional[dic
|
||||
Handles normalize peak, invert phase, swap channels, zero-crossing align, and synth wave generation.
|
||||
"""
|
||||
user_id = current_user["user_id"] if current_user else "anonymous"
|
||||
if current_user:
|
||||
enforce_password_changed(current_user)
|
||||
from app.core.python_tools_engine import PythonToolsEngine
|
||||
from app.core.ai_dsp_engine import AIDSPEngine
|
||||
import soundfile as sf
|
||||
|
||||
+108
-14
@@ -1,10 +1,15 @@
|
||||
import uuid
|
||||
import time
|
||||
from fastapi import APIRouter, HTTPException, Header, Depends
|
||||
import threading
|
||||
from fastapi import APIRouter, HTTPException, Header, Depends, Request, Response
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from typing import Optional
|
||||
from app.models.user import get_db_connection
|
||||
from app.core.auth import hash_password, verify_password, create_token, decode_token, seed_admin
|
||||
from app.core.auth import (
|
||||
hash_password, verify_password, create_token, decode_token, seed_admin,
|
||||
COOKIE_NAME, X_AUTH_HEADER,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -21,10 +26,49 @@ class ChangePasswordRequest(BaseModel):
|
||||
old_password: str
|
||||
new_password: str
|
||||
|
||||
def get_current_user(authorization: Optional[str] = Header(None)):
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
# ── Brute-force guard: in-memory per-IP failed-login limiter ──
|
||||
_LOGIN_FAILURES = {} # ip -> [timestamps]
|
||||
_LOGIN_LOCK = threading.Lock()
|
||||
MAX_LOGIN_ATTEMPTS = 10
|
||||
LOGIN_WINDOW_SEC = 900 # 15 min
|
||||
LOGIN_BLOCK_SEC = 900
|
||||
|
||||
def _check_login_ratelimit(ip: str):
|
||||
now = time.time()
|
||||
with _LOGIN_LOCK:
|
||||
stamps = [t for t in _LOGIN_FAILURES.get(ip, []) if now - t < LOGIN_WINDOW_SEC]
|
||||
if len(stamps) >= MAX_LOGIN_ATTEMPTS:
|
||||
raise HTTPException(status_code=429, detail="Quá nhiều lần đăng nhập thất bại. Vui lòng thử lại sau 15 phút.")
|
||||
_LOGIN_FAILURES[ip] = stamps
|
||||
|
||||
def _record_login_failure(ip: str):
|
||||
now = time.time()
|
||||
with _LOGIN_LOCK:
|
||||
stamps = _LOGIN_FAILURES.setdefault(ip, [])
|
||||
stamps.append(now)
|
||||
_LOGIN_FAILURES[ip] = [t for t in stamps if now - t < LOGIN_WINDOW_SEC]
|
||||
|
||||
def _record_login_success(ip: str):
|
||||
with _LOGIN_LOCK:
|
||||
_LOGIN_FAILURES.pop(ip, None)
|
||||
|
||||
def _set_auth_cookie(response: Response, token: str):
|
||||
response.set_cookie(
|
||||
COOKIE_NAME, token,
|
||||
max_age=7 * 24 * 3600, httponly=True, samesite="lax",
|
||||
# path="/" (default); secure flag set by proxy when behind TLS
|
||||
)
|
||||
|
||||
def get_current_user(request: Request, authorization: Optional[str] = Header(None), x_auth_token: Optional[str] = Header(None)):
|
||||
token = None
|
||||
if authorization and authorization.startswith("Bearer "):
|
||||
token = authorization.split(" ")[1]
|
||||
elif x_auth_token:
|
||||
token = x_auth_token
|
||||
elif request.cookies.get(COOKIE_NAME):
|
||||
token = request.cookies.get(COOKIE_NAME)
|
||||
if not token:
|
||||
raise HTTPException(status_code=401, detail="Thiếu Token xác thực hoặc Token không hợp lệ")
|
||||
token = authorization.split(" ")[1]
|
||||
payload = decode_token(token)
|
||||
if not payload:
|
||||
raise HTTPException(status_code=401, detail="Token đã hết hạn hoặc không hợp lệ")
|
||||
@@ -39,7 +83,10 @@ def enforce_password_changed(user: dict):
|
||||
)
|
||||
|
||||
@router.post("/login")
|
||||
async def login(req: LoginRequest):
|
||||
async def login(req: LoginRequest, request: Request):
|
||||
client_ip = request.client.host if request.client else "unknown"
|
||||
_check_login_ratelimit(client_ip)
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
@@ -65,14 +112,17 @@ async def login(req: LoginRequest):
|
||||
conn.close()
|
||||
|
||||
if not user or not user["is_active"]:
|
||||
_record_login_failure(client_ip)
|
||||
raise HTTPException(status_code=400, detail="Tài khoản hoặc mật khẩu không chính xác")
|
||||
|
||||
if not verify_password(password, user["hashed_password"]):
|
||||
_record_login_failure(client_ip)
|
||||
raise HTTPException(status_code=400, detail="Tài khoản hoặc mật khẩu không chính xác")
|
||||
|
||||
token = create_token(user["id"], user["username"], user["role"], user["must_change_password"])
|
||||
|
||||
return {
|
||||
_record_login_success(client_ip)
|
||||
|
||||
resp = JSONResponse({
|
||||
"access_token": token,
|
||||
"user": {
|
||||
"id": user["id"],
|
||||
@@ -81,13 +131,24 @@ async def login(req: LoginRequest):
|
||||
"role": user["role"],
|
||||
"must_change_password": bool(user["must_change_password"])
|
||||
}
|
||||
}
|
||||
})
|
||||
_set_auth_cookie(resp, token)
|
||||
return resp
|
||||
|
||||
def _validate_password_strength(password: str):
|
||||
"""Minimal strength policy: >= 8 chars and not trivially common."""
|
||||
if len(password) < 8:
|
||||
raise HTTPException(status_code=400, detail="Mật khẩu phải có ít nhất 8 ký tự")
|
||||
lowered = password.lower()
|
||||
if lowered in ("admin123", "password", "12345678", "123456789", "qwerty123"):
|
||||
raise HTTPException(status_code=400, detail="Mật khẩu quá dễ đoán, vui lòng chọn mật khẩu khác")
|
||||
|
||||
@router.post("/register")
|
||||
async def register(req: RegisterRequest):
|
||||
async def register(req: RegisterRequest, request: Request):
|
||||
username = req.username.strip()
|
||||
email = req.email.strip()
|
||||
password = req.password.strip()
|
||||
_validate_password_strength(password)
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
@@ -115,7 +176,7 @@ async def register(req: RegisterRequest):
|
||||
conn.close()
|
||||
|
||||
token = create_token(user_id, username, "standard", False)
|
||||
return {
|
||||
resp = JSONResponse({
|
||||
"access_token": token,
|
||||
"user": {
|
||||
"id": user_id,
|
||||
@@ -124,7 +185,9 @@ async def register(req: RegisterRequest):
|
||||
"role": "standard",
|
||||
"must_change_password": False
|
||||
}
|
||||
}
|
||||
})
|
||||
_set_auth_cookie(resp, token)
|
||||
return resp
|
||||
|
||||
@router.post("/change-password")
|
||||
async def change_password(req: ChangePasswordRequest, current_user: dict = Depends(get_current_user)):
|
||||
@@ -153,10 +216,12 @@ async def change_password(req: ChangePasswordRequest, current_user: dict = Depen
|
||||
conn.close()
|
||||
|
||||
new_token = create_token(updated_user["id"], updated_user["username"], updated_user["role"], False)
|
||||
return {
|
||||
resp = JSONResponse({
|
||||
"message": "Đổi mật khẩu thành công!",
|
||||
"access_token": new_token
|
||||
}
|
||||
})
|
||||
_set_auth_cookie(resp, new_token)
|
||||
return resp
|
||||
|
||||
@router.get("/profile")
|
||||
async def get_profile(current_user: dict = Depends(get_current_user)):
|
||||
@@ -194,3 +259,32 @@ async def get_profile(current_user: dict = Depends(get_current_user)):
|
||||
"max_tracks": row["max_tracks"] or 16
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/first-time")
|
||||
async def auth_first_time():
|
||||
# "Lần đăng nhập đầu" = tài khoản admin vẫn dùng mật khẩu MẶC ĐỊNH
|
||||
# (chưa từng đổi). Sau khi đổi lần đầu → first_time = false → UI xóa
|
||||
# gợi ý username/mật khẩu (Tùy chọn - Admin có thể bỏ trống, lần đầu:
|
||||
# admin123, nút Điền nhanh).
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"SELECT id, hashed_password, must_change_password FROM users "
|
||||
"WHERE LOWER(role) = 'admin' ORDER BY created_at ASC LIMIT 1"
|
||||
)
|
||||
row = cur.fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
if not row:
|
||||
return {"first_time": True}
|
||||
still_default = False
|
||||
try:
|
||||
still_default = verify_password("admin123", row["hashed_password"])
|
||||
except Exception:
|
||||
still_default = False
|
||||
return {"first_time": bool(row["must_change_password"]) and still_default}
|
||||
except Exception:
|
||||
return {"first_time": True}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import os
|
||||
import platform
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from app.core.vst_scanner import NativePluginScanner
|
||||
from app.api.v1.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Thư mục mặc định theo OS (spec desktop VST 2026-08-08)
|
||||
DEFAULT_PLUGIN_DIRS = {
|
||||
"Linux": ["/opt/daw_engine/vst3", os.path.expanduser("~/.vst3")],
|
||||
"Windows": [os.path.expandvars(r"%ProgramFiles%\Common Files\VST3")],
|
||||
"Darwin": ["/Library/Audio/Plug-Ins/Components", os.path.expanduser("~/Library/Audio/Plug-Ins/Components")],
|
||||
}
|
||||
|
||||
|
||||
class PluginScanRequest(BaseModel):
|
||||
custom_directories: Optional[list] = None
|
||||
scan_formats: Optional[list] = None
|
||||
force_rescan: Optional[bool] = False
|
||||
|
||||
|
||||
@router.post("/plugins/scan")
|
||||
async def scan_desktop_plugins(req: PluginScanRequest, current_user: dict = Depends(get_current_user)):
|
||||
"""Quét VST3/AU từ thư mục custom — spec: POST /api/v1/desktop/plugins/scan."""
|
||||
os_name = platform.system()
|
||||
dirs = req.custom_directories or DEFAULT_PLUGIN_DIRS.get(os_name, ["/opt/daw_engine/vst3"])
|
||||
if isinstance(dirs, str):
|
||||
dirs = [dirs]
|
||||
scanner = NativePluginScanner()
|
||||
plugins = scanner.scan_directories(dirs)
|
||||
return {
|
||||
"status": "success",
|
||||
"os": os_name,
|
||||
"scanned_directories": dirs,
|
||||
"total_found": len(plugins),
|
||||
"plugins": plugins,
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import os
|
||||
import platform
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from app.api.v1.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
MEDIA_EXTS = {
|
||||
".wav", ".mp3", ".ogg", ".flac", ".aiff", ".aif", ".m4a", ".aac", ".opus",
|
||||
".mid", ".midi"
|
||||
}
|
||||
|
||||
AUDIO_EXTS = {".wav", ".mp3", ".ogg", ".flac", ".aiff", ".aif", ".m4a", ".aac", ".opus"}
|
||||
MIDI_EXTS = {".mid", ".midi"}
|
||||
|
||||
|
||||
def _safe_path(path: str) -> str:
|
||||
if not path:
|
||||
raise HTTPException(status_code=400, detail="Thiếu path")
|
||||
if "\x00" in path:
|
||||
raise HTTPException(status_code=400, detail="Path không hợp lệ")
|
||||
return os.path.normpath(path)
|
||||
|
||||
|
||||
REAL_FS_TYPES = {
|
||||
"ext2", "ext3", "ext4", "xfs", "btrfs", "jfs", "reiserfs",
|
||||
"ntfs", "ntfs3", "vfat", "exfat", "fat", "hfs", "hfsplus", "apfs",
|
||||
"zfs", "f2fs", "iso9660", "udf", "nfs", "nfs4", "cifs", "smb3", "fuseblk",
|
||||
}
|
||||
|
||||
PSEUDO_FS_TYPES = {
|
||||
"proc", "sysfs", "devpts", "tmpfs", "devtmpfs", "overlay", "squashfs",
|
||||
"cgroup", "cgroup2", "pstore", "securityfs", "debugfs", "tracefs",
|
||||
"configfs", "fusectl", "hugetlbfs", "mqueue", "binfmt_misc", "nsfs",
|
||||
"autofs", "ramfs", "efivarfs", "rpc_pipefs", "fuse", "fusefs",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/computer")
|
||||
async def list_computer_roots(current_user: dict = Depends(get_current_user)):
|
||||
"""Liệt kê các ổ đĩa / mount point thật của máy (My Computer)."""
|
||||
system = platform.system()
|
||||
roots = []
|
||||
if system == "Windows":
|
||||
import string
|
||||
for drive in string.ascii_uppercase:
|
||||
root = drive + ":\\"
|
||||
try:
|
||||
if os.path.exists(root):
|
||||
roots.append({"path": root, "name": drive + ":", "is_dir": True})
|
||||
except OSError:
|
||||
continue
|
||||
else:
|
||||
# Unix/Linux/macOS: chỉ liệt kê filesystem thật, bỏ pseudo/docker/systemd mounts
|
||||
seen = set()
|
||||
try:
|
||||
with open("/proc/mounts", "r") as f:
|
||||
for line in f:
|
||||
parts = line.split()
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
device, mount, fstype = parts[0], parts[1], parts[2]
|
||||
if fstype in PSEUDO_FS_TYPES:
|
||||
continue
|
||||
if fstype not in REAL_FS_TYPES:
|
||||
# giữ mount point root "/" nếu không thuộc pseudo
|
||||
if mount != "/":
|
||||
continue
|
||||
if mount in seen:
|
||||
continue
|
||||
seen.add(mount)
|
||||
# lọc mount point rác kiểu /run/credentials/...
|
||||
if mount.startswith("/run/") or mount.startswith("/var/lib/docker"):
|
||||
continue
|
||||
try:
|
||||
if os.path.isdir(mount):
|
||||
label = mount if mount != "/" else "Root (/)"
|
||||
roots.append({"path": mount, "name": label, "is_dir": True})
|
||||
except OSError:
|
||||
pass
|
||||
except OSError:
|
||||
pass
|
||||
# macOS fallback: liệt kê /Volumes
|
||||
if system == "Darwin":
|
||||
try:
|
||||
for name in sorted(os.listdir("/Volumes")):
|
||||
full = os.path.join("/Volumes", name)
|
||||
if os.path.isdir(full):
|
||||
roots.append({"path": full, "name": name, "is_dir": True})
|
||||
except OSError:
|
||||
pass
|
||||
if not roots:
|
||||
roots = [{"path": "/", "name": "Root (/)", "is_dir": True}]
|
||||
return {"system": system, "roots": roots}
|
||||
|
||||
|
||||
@router.get("/browse")
|
||||
async def browse_directory(path: str = Query(...), current_user: dict = Depends(get_current_user)):
|
||||
"""Liệt kê nội dung một thư mục trên máy: thư mục con + file audio/MIDI."""
|
||||
resolved = _safe_path(path)
|
||||
if not os.path.isdir(resolved):
|
||||
raise HTTPException(status_code=404, detail="Không tìm thấy thư mục")
|
||||
|
||||
dirs, files = [], []
|
||||
try:
|
||||
entries = os.listdir(resolved)
|
||||
except OSError as e:
|
||||
raise HTTPException(status_code=403, detail=f"Không thể đọc thư mục: {e}")
|
||||
|
||||
for name in entries:
|
||||
if name.startswith("."):
|
||||
continue
|
||||
full = os.path.join(resolved, name)
|
||||
try:
|
||||
if os.path.isdir(full):
|
||||
dirs.append({"name": name, "path": full, "is_dir": True})
|
||||
else:
|
||||
ext = os.path.splitext(name)[1].lower()
|
||||
try:
|
||||
size = os.path.getsize(full)
|
||||
except OSError:
|
||||
size = 0
|
||||
kind = "midi" if ext in MIDI_EXTS else ("audio" if ext in AUDIO_EXTS else "other")
|
||||
files.append({
|
||||
"name": name,
|
||||
"path": full,
|
||||
"is_dir": False,
|
||||
"size_mb": round(size / (1024 * 1024), 2),
|
||||
"ext": ext,
|
||||
"kind": kind
|
||||
})
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
dirs.sort(key=lambda d: d["name"].lower())
|
||||
files.sort(key=lambda f: f["name"].lower())
|
||||
parent = os.path.dirname(resolved)
|
||||
return {
|
||||
"path": resolved,
|
||||
"parent": parent if parent != resolved else None,
|
||||
"dirs": dirs,
|
||||
"files": files
|
||||
}
|
||||
|
||||
|
||||
@router.get("/file")
|
||||
async def serve_local_file(path: str = Query(...), current_user: dict = Depends(get_current_user)):
|
||||
"""Phục vụ file audio/MIDI cục bộ để preview."""
|
||||
resolved = _safe_path(path)
|
||||
if not os.path.isfile(resolved):
|
||||
raise HTTPException(status_code=404, detail="Không tìm thấy file")
|
||||
ext = os.path.splitext(resolved)[1].lower()
|
||||
if ext not in MEDIA_EXTS:
|
||||
raise HTTPException(status_code=403, detail="Loại file không được hỗ trợ preview")
|
||||
media_type = "audio/wav" if ext in AUDIO_EXTS else "audio/midi"
|
||||
return FileResponse(resolved, media_type=media_type, filename=os.path.basename(resolved))
|
||||
@@ -54,7 +54,7 @@ async def mix_multitrack_session(req: MultitrackSessionRequest):
|
||||
|
||||
# Gửi task xuống Celery Worker
|
||||
from app.tasks.worker import mix_multitrack_task
|
||||
task = mix_multitrack_task.delay(req.dict())
|
||||
task = mix_multitrack_task.delay(req.model_dump())
|
||||
|
||||
return {
|
||||
"task_id": task.id,
|
||||
@@ -69,7 +69,7 @@ async def process_session(req: MultitrackSessionRequest):
|
||||
Xử lý từng clip, sau đó hòa âm tất cả tracks lại với nhau.
|
||||
"""
|
||||
from app.tasks.worker import process_multitrack_session_task
|
||||
task = process_multitrack_session_task.delay(req.dict())
|
||||
task = process_multitrack_session_task.delay(req.model_dump())
|
||||
|
||||
return {
|
||||
"task_id": task.id,
|
||||
|
||||
+50
-11
@@ -9,7 +9,7 @@ from app.core.render_engine import PythonRenderEngine
|
||||
from app.core.soundfont_inspector import SoundFontInspector
|
||||
from app.core.soundfont_converter import SoundFontConverter
|
||||
from app.core.soundfont_scanner import SoundFontAutoScanner
|
||||
from app.api.v1.auth import get_current_user
|
||||
from app.api.v1.auth import get_current_user, enforce_password_changed
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -18,6 +18,7 @@ os.makedirs(UPLOAD_SF_DIR, exist_ok=True)
|
||||
|
||||
SYSTEM_SF_DIR = "/opt/daw_engine/soundfonts"
|
||||
|
||||
|
||||
_inspector = None
|
||||
_scanner = None
|
||||
|
||||
@@ -79,11 +80,23 @@ async def upload_soundfont(
|
||||
background_tasks: BackgroundTasks = None,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
enforce_password_changed(current_user)
|
||||
if not (file.filename and (file.filename.endswith(".sf2") or file.filename.endswith(".sf3"))):
|
||||
raise HTTPException(status_code=400, detail="Only .sf2 / .sf3 files are allowed")
|
||||
|
||||
contents = await file.read()
|
||||
if not PluginManager.validate_sf2_header(contents):
|
||||
# Stream upload in chunks with a hard size cap (SGM-class fonts can exceed
|
||||
# 500MB; reading the whole body into RAM would OOM the server).
|
||||
MAX_SF_UPLOAD_BYTES = 2 * 1024 * 1024 * 1024 # 2 GB
|
||||
contents = bytearray()
|
||||
while True:
|
||||
chunk = await file.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
contents.extend(chunk)
|
||||
if len(contents) > MAX_SF_UPLOAD_BYTES:
|
||||
raise HTTPException(status_code=413, detail="SoundFont quá lớn (giới hạn 2GB)")
|
||||
|
||||
if not PluginManager.validate_sf2_header(bytes(contents[:4096])):
|
||||
raise HTTPException(status_code=400, detail="Invalid SoundFont file: missing RIFF/sfbk header")
|
||||
|
||||
file_ext = os.path.splitext(file.filename)[1]
|
||||
@@ -139,15 +152,36 @@ async def delete_soundfont(sf_id: str, current_user: dict = Depends(get_current_
|
||||
@router.get("/soundfonts/download/{sf_id}")
|
||||
async def download_soundfont_asset(sf_id: str):
|
||||
clean_id = sf_id.replace("sf_", "") if sf_id.startswith("sf_") else sf_id
|
||||
for base_dir in [UPLOAD_SF_DIR, SYSTEM_SF_DIR]:
|
||||
# Cũng tìm trong static/soundfonts (font bundled theo deployment) — trước
|
||||
# đây chỉ UPLOAD + SYSTEM → font bundled 404 → incognito (IndexedDB rỗng)
|
||||
# không tải được font → instrument CÂM (browser thường dùng cache nên OK).
|
||||
static_sf_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "static", "soundfonts")
|
||||
for base_dir in [UPLOAD_SF_DIR, SYSTEM_SF_DIR, static_sf_dir]:
|
||||
if not os.path.isdir(base_dir):
|
||||
continue
|
||||
for ext in [".sf2", ".sf3"]:
|
||||
for fname in os.listdir(base_dir):
|
||||
fbase, fext = os.path.splitext(fname)
|
||||
if fext.lower() == ext and fbase.lower() == clean_id.lower():
|
||||
full = os.path.join(base_dir, fname)
|
||||
return FileResponse(full, media_type="application/octet-stream", filename=f"soundfont{ext}")
|
||||
# Prefer SF2: the client FluidSynth WASM cannot decode SF3 (Ogg Vorbis)
|
||||
# samples, so any SF3 would play silence in the browser.
|
||||
for fname in os.listdir(base_dir):
|
||||
fbase, fext = os.path.splitext(fname)
|
||||
if fext.lower() == ".sf2" and fbase.lower() == clean_id.lower():
|
||||
full = os.path.join(base_dir, fname)
|
||||
return FileResponse(full, media_type="application/octet-stream", filename="soundfont.sf2")
|
||||
# Only an SF3 exists -> decompress it to a playable SF2 on demand (cached)
|
||||
for fname in os.listdir(base_dir):
|
||||
fbase, fext = os.path.splitext(fname)
|
||||
if fext.lower() == ".sf3" and fbase.lower() == clean_id.lower():
|
||||
full = os.path.join(base_dir, fname)
|
||||
try:
|
||||
from app.core.soundfont_converter import SoundFontConverter
|
||||
sf2_path = os.path.join(UPLOAD_SF_DIR, clean_id + ".sf2")
|
||||
if os.path.exists(sf2_path) and os.path.getmtime(sf2_path) >= os.path.getmtime(full):
|
||||
return FileResponse(sf2_path, media_type="application/octet-stream", filename="soundfont.sf2")
|
||||
result = SoundFontConverter().sf3_to_sf2(full, sf2_path)
|
||||
if result != full and os.path.exists(result):
|
||||
return FileResponse(result, media_type="application/octet-stream", filename="soundfont.sf2")
|
||||
except Exception as e:
|
||||
print(f"[soundfont-download] SF3->SF2 conversion failed for {full}: {e}")
|
||||
return FileResponse(full, media_type="application/octet-stream", filename="soundfont.sf3")
|
||||
raise HTTPException(status_code=404, detail="SoundFont asset not found")
|
||||
|
||||
|
||||
@@ -161,8 +195,13 @@ async def render_project(
|
||||
req: RenderRequest,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
enforce_password_changed(current_user)
|
||||
engine = PythonRenderEngine()
|
||||
output_path = os.path.join(settings.PROCESSED_DIR, req.output_filename or "render_output.wav")
|
||||
# Prevent path traversal: strip any directory components and force .wav.
|
||||
safe_name = os.path.basename((req.output_filename or "render_output.wav").replace("\\", "/"))
|
||||
if not safe_name.lower().endswith(".wav"):
|
||||
safe_name += ".wav"
|
||||
output_path = os.path.join(settings.PROCESSED_DIR, safe_name)
|
||||
try:
|
||||
result_path = engine.render_project(req.project_json, output_path)
|
||||
return {"url": f"/static/audio/processed/{os.path.basename(result_path)}", "path": result_path}
|
||||
|
||||
+31
-9
@@ -18,6 +18,11 @@ def upgrade_project_json_if_needed(project_data: dict) -> dict:
|
||||
return project_data
|
||||
|
||||
tracks = project_data.get("tracks", [])
|
||||
# Legacy format stores item start times in SECONDS; convert using the real
|
||||
# seconds-per-bar (old code hardcoded /4.0 which shifted every item's
|
||||
# position for any tempo other than the one where 1 bar = 4s).
|
||||
bpm_val = float(project_data.get("bpm", 120.0) or 120.0)
|
||||
seconds_per_bar = (60.0 / bpm_val) * 4
|
||||
upgraded_tracks = []
|
||||
for t in tracks:
|
||||
track_id = str(t.get("id", ""))
|
||||
@@ -33,8 +38,8 @@ def upgrade_project_json_if_needed(project_data: dict) -> dict:
|
||||
"id": c.get("id"),
|
||||
"name": c.get("name", "Audio Clip"),
|
||||
"type": "AUDIO_ITEM",
|
||||
"start_bar": c.get("startTime", 0.0) / 4.0,
|
||||
"duration_bars": 4.0,
|
||||
"start_bar": round(c.get("startTime", 0.0) / seconds_per_bar, 6),
|
||||
"duration_bars": round((c.get("duration", 4.0) if c.get("duration") else 4.0) / seconds_per_bar, 6),
|
||||
"clip_start_offset_bars": 0.0,
|
||||
"source_data": {
|
||||
"audio_file_url": f"/static/audio/uploads/{t.get('serverFileId')}" if t.get("serverFileId") else "",
|
||||
@@ -49,11 +54,11 @@ def upgrade_project_json_if_needed(project_data: dict) -> dict:
|
||||
"id": m.get("id"),
|
||||
"name": m.get("name", "MIDI Item"),
|
||||
"type": "MIDI_ITEM",
|
||||
"start_bar": m.get("startTime", 0.0) / 4.0,
|
||||
"duration_bars": m.get("duration", 4.0),
|
||||
"start_bar": round(m.get("startTime", 0.0) / seconds_per_bar, 6),
|
||||
"duration_bars": round((m.get("duration", 4.0) or 4.0) / seconds_per_bar, 6),
|
||||
"clip_start_offset_bars": 0.0,
|
||||
"source_data": {
|
||||
"total_buffer_bars": m.get("duration", 8.0),
|
||||
"total_buffer_bars": round((m.get("duration", 8.0) or 8.0) / seconds_per_bar, 6),
|
||||
"notes": m.get("notes", [])
|
||||
}
|
||||
})
|
||||
@@ -283,13 +288,30 @@ async def update_cloud_project(project_id: str, req: SaveProjectRequest, current
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT id FROM projects WHERE id = ? AND user_id = ? AND is_temp = 0", (project_id, user_id))
|
||||
exists = cursor.fetchone()
|
||||
if not exists:
|
||||
cursor.execute("SELECT id, size_bytes FROM projects WHERE id = ? AND user_id = ? AND is_temp = 0", (project_id, user_id))
|
||||
existing = cursor.fetchone()
|
||||
if not existing:
|
||||
conn.close()
|
||||
raise HTTPException(status_code=404, detail="Không tìm thấy dự án để cập nhật")
|
||||
|
||||
|
||||
new_size_bytes = len(validated_data_json.encode("utf-8"))
|
||||
|
||||
# Enforce storage quota (same rule as save_cloud_project — previously
|
||||
# update bypassed the quota entirely).
|
||||
cursor.execute("SELECT storage_limit_mb FROM user_quotas WHERE user_id = ?", (user_id,))
|
||||
quota_row = cursor.fetchone()
|
||||
storage_limit_mb = quota_row["storage_limit_mb"] if quota_row else 500
|
||||
cursor.execute("SELECT SUM(size_bytes) as total_used FROM projects WHERE user_id = ? AND is_temp = 0", (user_id,))
|
||||
used_row = cursor.fetchone()
|
||||
used_bytes = (used_row["total_used"] if used_row and used_row["total_used"] else 0) - (existing["size_bytes"] or 0)
|
||||
max_bytes = storage_limit_mb * 1024 * 1024
|
||||
if used_bytes + new_size_bytes > max_bytes:
|
||||
conn.close()
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Dung lượng dự án vượt quá hạn mức Quota ({storage_limit_mb}MB). Vui lòng dọn dẹp hoặc nâng cấp tài khoản."
|
||||
)
|
||||
|
||||
now = time.time()
|
||||
|
||||
cursor.execute("""
|
||||
|
||||
@@ -96,7 +96,7 @@ async def get_user_ai_config(authorization: Optional[str] = Header(None)):
|
||||
async def save_user_ai_config(req: SaveAIConfigRequest, authorization: Optional[str] = Header(None)):
|
||||
uid = _get_user_id(authorization)
|
||||
configs = _load_ai_configs()
|
||||
configs[uid] = [p.dict() for p in req.providers]
|
||||
configs[uid] = [p.model_dump() for p in req.providers]
|
||||
_save_all(ai_configs=configs)
|
||||
return {
|
||||
"success": True,
|
||||
|
||||
+30
-1
@@ -10,7 +10,36 @@ from typing import Optional, Dict, Any
|
||||
from app.models.user import get_db_connection
|
||||
from app.config import settings
|
||||
|
||||
SECRET_KEY = os.getenv("SECRET_KEY", "sonicforge_secret_key_super_secure_2026")
|
||||
COOKIE_NAME = "sf_token"
|
||||
X_AUTH_HEADER = "X-Auth-Token"
|
||||
|
||||
def _load_or_create_secret_key() -> str:
|
||||
"""Persistent random SECRET_KEY.
|
||||
|
||||
Priority: env SECRET_KEY > {STORAGE_DIR}/.secret_key (auto-generated on
|
||||
first run). Never falls back to a hardcoded value: a known secret lets
|
||||
anyone forge admin tokens.
|
||||
"""
|
||||
env_key = os.getenv("SECRET_KEY", "").strip()
|
||||
if env_key:
|
||||
return env_key
|
||||
key_file = os.path.join(settings.STORAGE_DIR, ".secret_key")
|
||||
try:
|
||||
os.makedirs(settings.STORAGE_DIR, exist_ok=True)
|
||||
if os.path.exists(key_file):
|
||||
with open(key_file, "r") as f:
|
||||
key = f.read().strip()
|
||||
if len(key) >= 32:
|
||||
return key
|
||||
key = secrets.token_hex(32)
|
||||
with open(key_file, "w") as f:
|
||||
f.write(key)
|
||||
return key
|
||||
except Exception:
|
||||
# Last resort: ephemeral random key (all tokens invalid on restart).
|
||||
return secrets.token_hex(32)
|
||||
|
||||
SECRET_KEY = _load_or_create_secret_key()
|
||||
|
||||
def hash_password(password: str, salt: Optional[str] = None) -> str:
|
||||
"""
|
||||
|
||||
+46
-22
@@ -1,4 +1,4 @@
|
||||
import os, logging
|
||||
import os, logging, math
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
import scipy.signal as signal
|
||||
@@ -87,18 +87,25 @@ class PythonRenderEngine:
|
||||
return url_or_id
|
||||
return url_or_id
|
||||
|
||||
def render_session_container(self, session: dict, section_store: dict, bpm: float, time_sig_num: int, total_samples: int) -> np.ndarray:
|
||||
def render_session_container(self, session: dict, section_store: dict, bpm: float, time_sig_num: int, total_samples: int, _cache: dict = None) -> np.ndarray:
|
||||
session_buffer = np.zeros((2, total_samples), dtype=np.float32)
|
||||
|
||||
# Solo semantics: when any track is soloed, only soloed tracks sound.
|
||||
tracks = session.get("tracks", [])
|
||||
solo_ids = {t.get("id") for t in tracks if t.get("solo")}
|
||||
|
||||
_channel_counter = 0
|
||||
|
||||
for track in session.get("tracks", []):
|
||||
for track in tracks:
|
||||
if solo_ids and track.get("id") not in solo_ids:
|
||||
continue
|
||||
track_type = track.get("type", "AUDIO")
|
||||
track_buffer = np.zeros((2, total_samples), dtype=np.float32)
|
||||
|
||||
# Parse synth_engine struct (Task C) — fall back to flat fields
|
||||
se = track.get("synth_engine", {}) or {}
|
||||
instrument_id = se.get("plugin_id") or track.get("instrument_id", "") or track.get("instrument", "")
|
||||
vst_path = se.get("plugin_path") or "" # spec desktop: load VST3 từ plugin_path
|
||||
instrument_source = se.get("type") or track.get("instrument_source", "soundfont")
|
||||
soundfont_bank = se.get("soundfont_bank") if se.get("soundfont_bank") is not None else track.get("soundfont_bank", 0)
|
||||
soundfont_program = se.get("soundfont_program") if se.get("soundfont_program") is not None else track.get("soundfont_program", 0)
|
||||
@@ -123,8 +130,17 @@ class PythonRenderEngine:
|
||||
try:
|
||||
audio_data, sr = sf.read(resolved_path, dtype='float32')
|
||||
if sr != self.sample_rate:
|
||||
# Resampling fallback if simple, otherwise skip
|
||||
pass
|
||||
# Proper resampling: previously a silent no-op that
|
||||
# played 48kHz audio at the wrong speed/pitch.
|
||||
from scipy.signal import resample_poly
|
||||
g = math.gcd(sr, self.sample_rate)
|
||||
audio_data = resample_poly(
|
||||
audio_data,
|
||||
up=self.sample_rate // g,
|
||||
down=sr // g,
|
||||
axis=-1,
|
||||
)
|
||||
sr = self.sample_rate
|
||||
|
||||
# Handle channel mapping (Mono/Stereo)
|
||||
if len(audio_data.shape) == 1:
|
||||
@@ -148,7 +164,7 @@ class PythonRenderEngine:
|
||||
if actual_len > 0:
|
||||
track_buffer[:, start_sample:write_end] += sliced_audio[:, :actual_len]
|
||||
except Exception as e:
|
||||
print(f"[RenderEngine] Error reading audio file {resolved_path}: {e}")
|
||||
logger.warning("[RenderEngine] Error reading audio file %s: %s", resolved_path, e)
|
||||
|
||||
elif item_type == "MIDI_ITEM":
|
||||
source_data = item.get("source_data", {})
|
||||
@@ -174,7 +190,8 @@ class PythonRenderEngine:
|
||||
if midi_events:
|
||||
try:
|
||||
plugin_mgr = PluginManager()
|
||||
vst = plugin_mgr.load_vst(instrument_id) if instrument_id else None
|
||||
# Ưu tiên plugin_path (spec desktop) — fallback plugin_id theo tên
|
||||
vst = plugin_mgr.load_vst(vst_path or instrument_id) if (vst_path or instrument_id) else None
|
||||
|
||||
if instrument_source == "pianobook":
|
||||
dspreset_path = track.get("dspreset_path", "")
|
||||
@@ -284,21 +301,27 @@ class PythonRenderEngine:
|
||||
actual_len = min(synth_buffer.shape[1], total_samples)
|
||||
track_buffer[:, :actual_len] += synth_buffer[:, :actual_len]
|
||||
except Exception as e:
|
||||
print(f"[RenderEngine] Error rendering MIDI: {e}")
|
||||
logger.warning("[RenderEngine] Error rendering MIDI: %s", e)
|
||||
|
||||
elif item_type == "SECTION_ITEM":
|
||||
source_data = item.get("source_data", {})
|
||||
sec_id = source_data.get("referenced_section_id", "")
|
||||
if sec_id and sec_id in section_store:
|
||||
# Render nested section recursively
|
||||
sec_container = section_store[sec_id]
|
||||
sec_buffer = self.render_session_container(
|
||||
session=sec_container,
|
||||
section_store=section_store,
|
||||
bpm=bpm,
|
||||
time_sig_num=time_sig_num,
|
||||
total_samples=total_samples
|
||||
)
|
||||
# Render nested section recursively, cached per section id
|
||||
# so repeated section instances don't re-render every time.
|
||||
cache = _cache if _cache is not None else {}
|
||||
if sec_id in cache:
|
||||
sec_buffer = cache[sec_id]
|
||||
else:
|
||||
sec_buffer = self.render_session_container(
|
||||
session=section_store[sec_id],
|
||||
section_store=section_store,
|
||||
bpm=bpm,
|
||||
time_sig_num=time_sig_num,
|
||||
total_samples=total_samples,
|
||||
_cache=cache,
|
||||
)
|
||||
cache[sec_id] = sec_buffer
|
||||
|
||||
# Apply non-destructive crop/slicing on section buffer
|
||||
if offset_sample < total_samples:
|
||||
@@ -327,7 +350,7 @@ class PythonRenderEngine:
|
||||
board = Pedalboard([Chorus(rate_hz=1.5, depth=0.25)])
|
||||
track_buffer = board(track_buffer, sample_rate=self.sample_rate)
|
||||
except Exception as e:
|
||||
print(f"[RenderEngine] Pedalboard Chorus failed: {e}")
|
||||
logger.warning("[RenderEngine] Pedalboard Chorus failed: %s", e)
|
||||
else:
|
||||
# Fallback chorus using simple LFO delay modulation in scipy/numpy
|
||||
try:
|
||||
@@ -341,14 +364,14 @@ class PythonRenderEngine:
|
||||
wet[ch, :] = track_buffer[ch, indices]
|
||||
track_buffer = dry + wet * 0.5
|
||||
except Exception as e:
|
||||
print(f"[RenderEngine] Fallback Chorus failed: {e}")
|
||||
logger.warning("[RenderEngine] Fallback Chorus failed: %s", e)
|
||||
elif fx_type == "reverb":
|
||||
if HAS_PEDALBOARD:
|
||||
try:
|
||||
board = Pedalboard([Reverb(room_size=0.5, wet_level=0.4, dry_level=0.6)])
|
||||
track_buffer = board(track_buffer, sample_rate=self.sample_rate)
|
||||
except Exception as e:
|
||||
print(f"[RenderEngine] Pedalboard Reverb failed: {e}")
|
||||
logger.warning("[RenderEngine] Pedalboard Reverb failed: %s", e)
|
||||
else:
|
||||
# Fallback reverb using exponentially decaying noise room impulse response
|
||||
try:
|
||||
@@ -368,7 +391,7 @@ class PythonRenderEngine:
|
||||
wet[ch, :] = conv
|
||||
track_buffer = dry + wet * 0.4
|
||||
except Exception as e:
|
||||
print(f"[RenderEngine] Fallback Reverb failed: {e}")
|
||||
logger.warning("[RenderEngine] Fallback Reverb failed: %s", e)
|
||||
|
||||
# Process track volume
|
||||
if HAS_PEDALBOARD:
|
||||
@@ -410,7 +433,8 @@ class PythonRenderEngine:
|
||||
section_store=section_store,
|
||||
bpm=bpm,
|
||||
time_sig_num=time_sig_num,
|
||||
total_samples=total_samples
|
||||
total_samples=total_samples,
|
||||
_cache={},
|
||||
)
|
||||
|
||||
# Normalization to prevent clipping
|
||||
|
||||
+339
-39
@@ -83,6 +83,35 @@ class SoundFontConverter:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _encode_sample_ogg(self, pcm: bytes, rate: int, tmp_dir: str) -> bytes:
|
||||
"""Encode a single mono 16-bit PCM slice to an Ogg Vorbis stream."""
|
||||
tmp_wav = os.path.join(tmp_dir, "sample_tmp.wav")
|
||||
tmp_ogg = os.path.join(tmp_dir, "sample_tmp.ogg")
|
||||
try:
|
||||
with open(tmp_wav, "wb") as fw:
|
||||
with wave.open(fw, "wb") as w:
|
||||
w.setnchannels(1)
|
||||
w.setsampwidth(2)
|
||||
w.setframerate(rate)
|
||||
w.writeframes(pcm)
|
||||
subprocess.run([
|
||||
"ffmpeg", "-y", "-i", tmp_wav,
|
||||
"-c:a", "libvorbis", "-q:a", "3",
|
||||
"-f", "ogg", tmp_ogg
|
||||
], capture_output=True, timeout=600, check=True)
|
||||
with open(tmp_ogg, "rb") as fo:
|
||||
return fo.read()
|
||||
except Exception as e:
|
||||
logger.error(f"Sample OGG encode failed: {e}")
|
||||
return b""
|
||||
finally:
|
||||
for p in [tmp_wav, tmp_ogg]:
|
||||
try:
|
||||
if os.path.exists(p):
|
||||
os.remove(p)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _sf2_to_sf3_python(self, sf2_path: str, sf3_path: str) -> bool:
|
||||
has_ogg = self._check_ffmpeg_ogg()
|
||||
if not has_ogg:
|
||||
@@ -100,7 +129,7 @@ class SoundFontConverter:
|
||||
logger.warning("Not a valid SF2 file")
|
||||
return False
|
||||
|
||||
# Find smpl chunk recursively
|
||||
# Find smpl chunk (PCM sample data) recursively
|
||||
smpl = _find_chunk(data, b"smpl")
|
||||
if smpl is None:
|
||||
logger.warning("No smpl chunk found in SF2")
|
||||
@@ -113,46 +142,103 @@ class SoundFontConverter:
|
||||
logger.warning("Sample data too small")
|
||||
return False
|
||||
|
||||
tmp_wav = sf3_path + ".tmp.wav"
|
||||
tmp_ogg = sf3_path + ".tmp.ogg"
|
||||
# Locate shdr (sample headers) inside the pdta LIST
|
||||
pdta = _find_list_of_type(data, b"pdta", 12)
|
||||
if not pdta:
|
||||
logger.warning("No pdta LIST found")
|
||||
return False
|
||||
_, _, pdta_size, pdta_data_off = pdta
|
||||
shdr = _find_chunk_in_list(data, b"shdr", pdta_data_off, pdta_size - 4)
|
||||
if not shdr:
|
||||
logger.warning("No shdr chunk found")
|
||||
return False
|
||||
_, _, shdr_size, shdr_data_off = shdr
|
||||
if shdr_size <= 0 or shdr_size % 46 != 0:
|
||||
logger.warning(f"Invalid shdr size {shdr_size}")
|
||||
return False
|
||||
n_samples = shdr_size // 46
|
||||
|
||||
# Locate ifil (version) inside the INFO LIST
|
||||
ifil_abs = None
|
||||
info = _find_list_of_type(data, b"INFO", 12)
|
||||
if info:
|
||||
_, _, info_size, info_data_off = info
|
||||
ifil = _find_chunk_in_list(data, b"ifil", info_data_off, info_size - 4)
|
||||
if ifil:
|
||||
ifil_abs = ifil[3]
|
||||
|
||||
import tempfile
|
||||
tmp_dir = tempfile.mkdtemp(prefix="sf3conv_")
|
||||
ogg_parts = []
|
||||
new_shdr = bytearray()
|
||||
byte_offset = 0
|
||||
ogg_bytes = 0
|
||||
try:
|
||||
# Write samples as WAV
|
||||
with open(tmp_wav, "wb") as fw:
|
||||
with wave.open(fw, "wb") as w:
|
||||
w.setnchannels(1)
|
||||
w.setsampwidth(2)
|
||||
w.setframerate(44100)
|
||||
w.writeframes(sample_data)
|
||||
for i in range(n_samples):
|
||||
base = shdr_data_off + i * 46
|
||||
# shdr layout: name[20] | start(u32) end(u32) loopstart(u32) loopend(u32) samplerate(i32) ...
|
||||
start, end, startloop, endloop, rate = struct.unpack("<IIIIi", data[base + 20:base + 40])
|
||||
sampletype = struct.unpack("<H", data[base + 44:base + 46])[0]
|
||||
ogg_stream = b""
|
||||
if end >= start and start * 2 < len(sample_data):
|
||||
pcm = sample_data[start * 2:(end + 1) * 2]
|
||||
if len(pcm) >= 4:
|
||||
safe_rate = rate if 1000 < rate < 192000 else 44100
|
||||
ogg_stream = self._encode_sample_ogg(pcm, safe_rate, tmp_dir)
|
||||
# SF3: start/end are byte offsets into the concatenated OGG stream.
|
||||
# FluidSynth treats shdr `end` as EXCLUSIVE (reads [start..end-1]),
|
||||
# so end = start + ogg length.
|
||||
new_start = byte_offset
|
||||
new_end = byte_offset + len(ogg_stream)
|
||||
# OGG loop pointers are relative to the individual decompressed sample
|
||||
new_sloop = (startloop - start) if (startloop > start and startloop <= end) else 0
|
||||
new_eloop = (endloop - start) if (endloop > start and endloop <= end) else 0
|
||||
# Mark the sample as Ogg Vorbis compressed (FLUID_SAMPLETYPE_OGG_VORBIS = 0x20)
|
||||
new_stype = sampletype | 0x20
|
||||
new_shdr += data[base:base + 20] # sample name
|
||||
new_shdr += struct.pack("<IIIIi", new_start, new_end, new_sloop, new_eloop, rate)
|
||||
new_shdr += data[base + 40:base + 44] # originalpitch, correction, samplelink
|
||||
new_shdr += struct.pack("<H", new_stype)
|
||||
ogg_parts.append(ogg_stream)
|
||||
ogg_bytes += len(ogg_stream)
|
||||
byte_offset = new_start + len(ogg_stream)
|
||||
if len(ogg_stream) % 2 == 1:
|
||||
ogg_parts.append(b"\x00")
|
||||
byte_offset += 1
|
||||
|
||||
# Compress to Ogg Vorbis
|
||||
subprocess.run([
|
||||
"ffmpeg", "-y", "-i", tmp_wav,
|
||||
"-c:a", "libvorbis", "-q:a", "3",
|
||||
"-f", "ogg", tmp_ogg
|
||||
], capture_output=True, timeout=600, check=True)
|
||||
if not ogg_parts:
|
||||
logger.warning("No samples to encode")
|
||||
return False
|
||||
|
||||
with open(tmp_ogg, "rb") as fo:
|
||||
ogg_data = fo.read()
|
||||
|
||||
compression = (1 - len(ogg_data) / max(len(sample_data), 1)) * 100
|
||||
logger.info(f"Compressed {len(sample_data)} -> {len(ogg_data)} bytes ({compression:.0f}%)")
|
||||
|
||||
ogg_padded = ogg_data if len(ogg_data) % 2 == 0 else ogg_data + b"\x00"
|
||||
new_smpl_size = len(ogg_data)
|
||||
ogg_padded = b"".join(ogg_parts)
|
||||
if len(ogg_padded) % 2 == 1:
|
||||
ogg_padded += b"\x00"
|
||||
new_smpl_size = len(ogg_padded)
|
||||
old_padded = smpl_old_size + (1 if smpl_old_size % 2 == 1 else 0)
|
||||
new_padded = len(ogg_padded)
|
||||
delta = new_padded - old_padded
|
||||
delta = len(ogg_padded) - old_padded
|
||||
|
||||
# Rebuild file: replace smpl chunk and update all sizes
|
||||
out = bytearray()
|
||||
out.extend(data[:smpl_head_off]) # up to smpl chunk header
|
||||
out.extend(data[:smpl_head_off]) # up to smpl chunk header (excl. id)
|
||||
out.extend(b"smpl") # chunk id (required for valid SF3)
|
||||
out.extend(struct.pack("<I", new_smpl_size)) # new smpl size
|
||||
out.extend(ogg_padded) # compressed data (even-padded)
|
||||
out.extend(data[smpl_head_off + 8 + old_padded:]) # rest of file
|
||||
out.extend(ogg_padded) # concatenated per-sample OGG streams
|
||||
rest_off = smpl_head_off + 8 + old_padded
|
||||
rest = bytearray(data[rest_off:])
|
||||
# Patch the shdr sample headers inside the pdta copy
|
||||
shdr_in_rest = shdr_data_off - rest_off
|
||||
if shdr_in_rest < 0 or shdr_in_rest + shdr_size > len(rest):
|
||||
logger.warning("shdr not found after smpl chunk")
|
||||
return False
|
||||
rest[shdr_in_rest:shdr_in_rest + shdr_size] = new_shdr
|
||||
out.extend(rest)
|
||||
|
||||
data_out = bytes(out)
|
||||
|
||||
# SF3 requires version 3.0 so FluidSynth treats it as an SF3 file
|
||||
if ifil_abs is not None and ifil_abs + 4 <= len(data_out):
|
||||
data_out = data_out[:ifil_abs] + struct.pack("<HH", 3, 0) + data_out[ifil_abs + 4:]
|
||||
|
||||
# Find sdta LIST and update its size
|
||||
sdta = _find_list_of_type(data_out, b"sdta", 12)
|
||||
if sdta:
|
||||
@@ -160,12 +246,11 @@ class SoundFontConverter:
|
||||
data_out = _update_size(data_out, lh_off + 4, lh_size + delta)
|
||||
|
||||
# Update RIFF root size
|
||||
old_riff_size = struct.unpack("<I", data_out[4:8])[0]
|
||||
new_total = len(data_out) - 8
|
||||
data_out = _update_size(data_out, 4, new_total)
|
||||
|
||||
with open(sf3_path, "wb") as fout:
|
||||
fout.write(bytes(data_out))
|
||||
fout.write(data_out)
|
||||
|
||||
# Validate: check that RIFF size matches actual size
|
||||
written = os.path.getsize(sf3_path)
|
||||
@@ -174,6 +259,7 @@ class SoundFontConverter:
|
||||
logger.warning(f"Size mismatch: RIFF says {parsed_riff}, actual is {written - 8}")
|
||||
return False
|
||||
|
||||
logger.info(f"Converted {n_samples} samples -> {ogg_bytes} bytes OGG ({100 * (1 - ogg_bytes / max(len(sample_data), 1)):.0f}% smaller)")
|
||||
return True
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
@@ -185,12 +271,50 @@ class SoundFontConverter:
|
||||
traceback.print_exc()
|
||||
return False
|
||||
finally:
|
||||
for p in [tmp_wav, tmp_ogg]:
|
||||
try:
|
||||
import shutil
|
||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _sf3_plays_audio(path: str) -> bool:
|
||||
"""Verify a SoundFont actually loads and renders audible audio (guards
|
||||
against shipping malformed SF3 files that silently play nothing).
|
||||
|
||||
Uses the low-level CFFI binding (new_fluid_synth / write_float) — the
|
||||
high-level Synth() class does not exist in this binding, so it is never
|
||||
used here.
|
||||
"""
|
||||
if not os.path.exists(path):
|
||||
return False
|
||||
try:
|
||||
import fluidsynth as _fs
|
||||
import numpy as np
|
||||
_settings = _fs.new_fluid_settings()
|
||||
_fl = _fs.new_fluid_synth(_settings)
|
||||
try:
|
||||
h = _fs.fluid_synth_sfload(_fl, path.encode("utf-8"), 1)
|
||||
if h < 0:
|
||||
return False
|
||||
_fs.fluid_synth_program_select(_fl, 0, h, 0, 0)
|
||||
_fs.fluid_synth_noteon(_fl, 0, 60, 100)
|
||||
frames = 8820 # 0.2s
|
||||
buf = np.zeros(frames * 2, dtype=np.float32)
|
||||
_fs.fluid_synth_write_float(
|
||||
_fl, frames, buf.ctypes.data, 0, 1,
|
||||
buf.ctypes.data + frames * 4, 0, 1
|
||||
)
|
||||
_fs.fluid_synth_noteoff(_fl, 0, 60)
|
||||
rms = float(np.sqrt(np.mean(buf ** 2)))
|
||||
return rms > 1e-4
|
||||
finally:
|
||||
try:
|
||||
if os.path.exists(p):
|
||||
os.remove(p)
|
||||
_fs.delete_fluid_synth(_fl)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _find_sf3_converter():
|
||||
@@ -208,9 +332,18 @@ class SoundFontConverter:
|
||||
|
||||
sf3_path = os.path.splitext(sf2_path)[0] + ".sf3"
|
||||
|
||||
# Reuse a working SF3 if it is newer than the SF2 AND actually plays
|
||||
# audio. Malformed SF3s (e.g. produced by an older converter) are
|
||||
# re-converted automatically instead of being shipped silently broken.
|
||||
if os.path.exists(sf3_path) and os.path.getmtime(sf3_path) >= os.path.getmtime(sf2_path):
|
||||
logger.info(f"SF3 already up-to-date: {sf3_path}")
|
||||
return sf3_path
|
||||
if self._sf3_plays_audio(sf3_path):
|
||||
logger.info(f"SF3 already up-to-date: {sf3_path}")
|
||||
return sf3_path
|
||||
logger.warning(f"Existing SF3 does not play audio, re-converting: {sf3_path}")
|
||||
try:
|
||||
os.remove(sf3_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
converter = self._find_sf3_converter()
|
||||
try:
|
||||
@@ -228,9 +361,16 @@ class SoundFontConverter:
|
||||
|
||||
if converter == "python":
|
||||
if self._sf2_to_sf3_python(sf2_path, sf3_path) and os.path.exists(sf3_path):
|
||||
size_mb = os.path.getsize(sf3_path) / (1024 * 1024)
|
||||
logger.info(f"Created SF3: {sf3_path} ({size_mb:.2f} MB)")
|
||||
return sf3_path
|
||||
if self._sf3_plays_audio(sf3_path):
|
||||
size_mb = os.path.getsize(sf3_path) / (1024 * 1024)
|
||||
logger.info(f"Created SF3: {sf3_path} ({size_mb:.2f} MB)")
|
||||
return sf3_path
|
||||
# Conversion produced a broken file — never ship it
|
||||
logger.warning(f"Converted SF3 failed audio verification, removing: {sf3_path}")
|
||||
try:
|
||||
os.remove(sf3_path)
|
||||
except Exception:
|
||||
pass
|
||||
logger.warning(f"Python converter failed for {sf2_path}, returning SF2 path")
|
||||
return sf2_path
|
||||
|
||||
@@ -242,6 +382,165 @@ class SoundFontConverter:
|
||||
logger.error(f"Error converting {sf2_path}: {e}")
|
||||
return sf2_path
|
||||
|
||||
def sf3_to_sf2(self, sf3_path: str, sf2_path: str = None) -> str:
|
||||
"""Convert an SF3 (Ogg Vorbis samples) soundfont into a playable SF2.
|
||||
|
||||
The client FluidSynth WASM cannot decode Ogg Vorbis/SF3 samples, so any
|
||||
SF3 soundfont (converted or uploaded) plays silence. Decompressing to
|
||||
SF2 makes every instrument audible again.
|
||||
"""
|
||||
if not os.path.exists(sf3_path):
|
||||
raise FileNotFoundError(f"Source SF3 file not found: {sf3_path}")
|
||||
|
||||
try:
|
||||
with open(sf3_path, "rb") as f:
|
||||
data = f.read()
|
||||
except Exception as e:
|
||||
raise IOError(f"Cannot read {sf3_path}: {e}")
|
||||
|
||||
if len(data) < 12 or data[:4] != b"RIFF" or data[8:12] != b"sfbk":
|
||||
raise ValueError(f"Not a valid SoundFont file: {sf3_path}")
|
||||
|
||||
smpl = _find_chunk(data, b"smpl")
|
||||
if not smpl:
|
||||
raise ValueError("No smpl chunk found")
|
||||
smpl_head_off, _, smpl_old_size, smpl_data_off = smpl
|
||||
sample_data = data[smpl_data_off:smpl_data_off + smpl_old_size]
|
||||
|
||||
pdta = _find_list_of_type(data, b"pdta", 12)
|
||||
if not pdta:
|
||||
raise ValueError("No pdta LIST found")
|
||||
_, _, pdta_size, pdta_data_off = pdta
|
||||
shdr = _find_chunk_in_list(data, b"shdr", pdta_data_off, pdta_size - 4)
|
||||
if not shdr:
|
||||
raise ValueError("No shdr chunk found")
|
||||
_, _, shdr_size, shdr_data_off = shdr
|
||||
if shdr_size <= 0 or shdr_size % 46 != 0:
|
||||
raise ValueError(f"Invalid shdr size {shdr_size}")
|
||||
n_samples = shdr_size // 46
|
||||
|
||||
ifil_abs = None
|
||||
info = _find_list_of_type(data, b"INFO", 12)
|
||||
if info:
|
||||
_, _, info_size, info_data_off = info
|
||||
ifil = _find_chunk_in_list(data, b"ifil", info_data_off, info_size - 4)
|
||||
if ifil:
|
||||
ifil_abs = ifil[3]
|
||||
|
||||
import tempfile
|
||||
tmp_dir = tempfile.mkdtemp(prefix="sf2conv_")
|
||||
pcm_parts = []
|
||||
new_shdr = bytearray()
|
||||
frame_offset = 0
|
||||
total_pcm_bytes = 0
|
||||
try:
|
||||
for i in range(n_samples):
|
||||
base = shdr_data_off + i * 46
|
||||
name = data[base:base + 20]
|
||||
start, end, loopstart, loopend, rate = struct.unpack("<IIIIi", data[base + 20:base + 40])
|
||||
sampletype = struct.unpack("<H", data[base + 44:base + 46])[0]
|
||||
frames = 0
|
||||
pcm = b""
|
||||
if end > start and start < len(sample_data):
|
||||
# FluidSynth reads the OGG region as [start..end-1]
|
||||
ogg = sample_data[start:min(end, len(sample_data))]
|
||||
if len(ogg) >= 4 and ogg[:4] == b"OggS":
|
||||
pcm = self._decode_sample_ogg(ogg, tmp_dir)
|
||||
frames = len(pcm) // 2
|
||||
new_start = frame_offset
|
||||
new_end = frame_offset + frames
|
||||
# SF3 loop points are relative to the decompressed sample; SF2 needs absolute
|
||||
new_loopstart = loopstart + new_start if (loopstart or loopend) else 0
|
||||
new_loopend = loopend + new_start if (loopstart or loopend) else 0
|
||||
# Clear the Ogg Vorbis flag; keep mono/left/right/linked flags
|
||||
new_stype = sampletype & ~0x20
|
||||
new_shdr += name
|
||||
new_shdr += struct.pack("<IIIIi", new_start, new_end, new_loopstart, new_loopend, rate)
|
||||
new_shdr += data[base + 40:base + 44]
|
||||
new_shdr += struct.pack("<H", new_stype)
|
||||
pcm_parts.append(pcm)
|
||||
frame_offset += frames
|
||||
if frames:
|
||||
total_pcm_bytes += frames * 2
|
||||
|
||||
if total_pcm_bytes == 0:
|
||||
logger.warning("SF3 contained no decodable samples")
|
||||
return sf3_path
|
||||
|
||||
new_smpl_size = total_pcm_bytes
|
||||
if new_smpl_size % 2 == 1:
|
||||
new_smpl_size += 1
|
||||
old_padded = smpl_old_size + (1 if smpl_old_size % 2 == 1 else 0)
|
||||
delta = new_smpl_size - old_padded
|
||||
|
||||
out = bytearray()
|
||||
out.extend(data[:smpl_head_off])
|
||||
out.extend(b"smpl")
|
||||
out.extend(struct.pack("<I", new_smpl_size))
|
||||
for part in pcm_parts:
|
||||
out.extend(part)
|
||||
if total_pcm_bytes % 2 == 1:
|
||||
out.extend(b"\x00")
|
||||
rest_off = smpl_head_off + 8 + old_padded
|
||||
rest = bytearray(data[rest_off:])
|
||||
shdr_in_rest = shdr_data_off - rest_off
|
||||
if shdr_in_rest < 0 or shdr_in_rest + shdr_size > len(rest):
|
||||
logger.warning("shdr not found after smpl chunk")
|
||||
return sf3_path
|
||||
rest[shdr_in_rest:shdr_in_rest + shdr_size] = new_shdr
|
||||
out.extend(rest)
|
||||
|
||||
data_out = bytes(out)
|
||||
|
||||
# Back to SF2 version 2.01
|
||||
if ifil_abs is not None and ifil_abs + 4 <= len(data_out):
|
||||
data_out = data_out[:ifil_abs] + struct.pack("<HH", 2, 1) + data_out[ifil_abs + 4:]
|
||||
|
||||
sdta = _find_list_of_type(data_out, b"sdta", 12)
|
||||
if sdta:
|
||||
lh_off, _, lh_size, ld_off = sdta
|
||||
data_out = _update_size(data_out, lh_off + 4, lh_size + delta)
|
||||
|
||||
data_out = _update_size(data_out, 4, len(data_out) - 8)
|
||||
|
||||
sf2_path = sf2_path or (os.path.splitext(sf3_path)[0] + ".sf2")
|
||||
with open(sf2_path, "wb") as fout:
|
||||
fout.write(data_out)
|
||||
logger.info(f"Converted SF3 -> SF2: {sf2_path} ({os.path.getsize(sf2_path) / 1048576:.1f} MB)")
|
||||
return sf2_path
|
||||
finally:
|
||||
try:
|
||||
import shutil
|
||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _decode_sample_ogg(self, ogg: bytes, tmp_dir: str) -> bytes:
|
||||
"""Decode an Ogg Vorbis stream to mono 16-bit PCM; returns PCM bytes."""
|
||||
tmp_ogg = os.path.join(tmp_dir, "sample.ogg")
|
||||
tmp_pcm = os.path.join(tmp_dir, "sample.pcm")
|
||||
try:
|
||||
with open(tmp_ogg, "wb") as fo:
|
||||
fo.write(ogg)
|
||||
r = subprocess.run(
|
||||
["ffmpeg", "-y", "-v", "error", "-i", tmp_ogg, "-f", "s16le", "-ac", "1", tmp_pcm],
|
||||
capture_output=True, timeout=600)
|
||||
if r.returncode != 0:
|
||||
logger.warning(f"OGG decode failed: {r.stderr.decode(errors='replace')[:120]}")
|
||||
return b""
|
||||
with open(tmp_pcm, "rb") as fp:
|
||||
return fp.read()
|
||||
except Exception as e:
|
||||
logger.warning(f"OGG decode error: {e}")
|
||||
return b""
|
||||
finally:
|
||||
for p in [tmp_ogg, tmp_pcm]:
|
||||
try:
|
||||
if os.path.exists(p):
|
||||
os.remove(p)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def batch_convert_all(self):
|
||||
for sdir in self.target_dirs:
|
||||
if not os.path.isdir(sdir):
|
||||
@@ -249,3 +548,4 @@ class SoundFontConverter:
|
||||
for fname in sorted(os.listdir(sdir)):
|
||||
if fname.lower().endswith(".sf2"):
|
||||
self.convert_sf2_to_sf3(os.path.join(sdir, fname))
|
||||
|
||||
|
||||
+75
-41
@@ -2,7 +2,7 @@
|
||||
import os
|
||||
import numpy as np
|
||||
import functools
|
||||
from ctypes import c_int, c_char_p, c_void_p
|
||||
from ctypes import c_char_p
|
||||
|
||||
def midi_note_to_freq(note_number: int) -> float:
|
||||
return 440.0 * (2.0 ** ((note_number - 69) / 12.0))
|
||||
@@ -110,7 +110,12 @@ def get_plugin_manager(vst_dir="/opt/daw_engine/vst3", sf_dir="/opt/daw_engine/s
|
||||
return _PLUGIN_MANAGER_INSTANCE
|
||||
|
||||
def load_soundfont_cached(path: str):
|
||||
"""Return a cached FluidSynth instance for path, incrementing refcount."""
|
||||
"""Return a cached low-level FluidSynth instance for path, incrementing refcount.
|
||||
|
||||
Uses the CFFI binding API (new_fluid_synth / fluid_synth_sfload) — the same
|
||||
API render_engine relies on. The high-level `FluidSynth()`/`Synth()` classes
|
||||
do not exist in this binding, so they are never used here.
|
||||
"""
|
||||
global _FLUID_CACHE
|
||||
if not HAS_PYFLUIDSYNTH:
|
||||
return None
|
||||
@@ -119,10 +124,15 @@ def load_soundfont_cached(path: str):
|
||||
_FLUID_CACHE[path] = (fl, ref + 1)
|
||||
return fl
|
||||
try:
|
||||
import fluidsynth
|
||||
fl = fluidsynth.FluidSynth(sample_rate=44100, gain=0.5)
|
||||
font_id = fl.sfload(path)
|
||||
fl.program_select(0, font_id, 0, 0)
|
||||
import fluidsynth as _fs
|
||||
_settings = _fs.new_fluid_settings()
|
||||
_fs.fluid_settings_setnum(_settings, b'synth.sample-rate', 44100.0)
|
||||
fl = _fs.new_fluid_synth(_settings)
|
||||
font_id = _fs.fluid_synth_sfload(fl, path.encode("utf-8"), 1)
|
||||
if font_id < 0:
|
||||
_fs.delete_fluid_synth(fl)
|
||||
return None
|
||||
_fs.fluid_synth_program_select(fl, 0, font_id, 0, 0)
|
||||
_FLUID_CACHE[path] = (fl, 1)
|
||||
return fl
|
||||
except Exception:
|
||||
@@ -136,7 +146,8 @@ def release_soundfont(path: str):
|
||||
fl, ref = _FLUID_CACHE[path]
|
||||
if ref <= 1:
|
||||
try:
|
||||
fl.delete()
|
||||
import fluidsynth as _fs
|
||||
_fs.delete_fluid_synth(fl)
|
||||
except Exception:
|
||||
pass
|
||||
del _FLUID_CACHE[path]
|
||||
@@ -205,6 +216,19 @@ class PluginManager:
|
||||
def load_vst(self, plugin_name: str, preset_data: dict = None):
|
||||
if not HAS_PEDALBOARD:
|
||||
return None
|
||||
# Ưu tiên PATH trực tiếp (spec desktop 2026-08-08: synth_engine.plugin_path)
|
||||
if plugin_name and (plugin_name.endswith(".vst3") or plugin_name.endswith(".so")
|
||||
or plugin_name.endswith(".component") or "/" in plugin_name or "\\" in plugin_name):
|
||||
if os.path.exists(plugin_name):
|
||||
vst = VST3Plugin(plugin_name)
|
||||
if preset_data:
|
||||
for k, v in preset_data.items():
|
||||
try:
|
||||
setattr(vst, k, v)
|
||||
except Exception:
|
||||
pass
|
||||
return vst
|
||||
return None
|
||||
plugins = self._scan_plugins()
|
||||
if plugin_name not in plugins:
|
||||
return None
|
||||
@@ -245,38 +269,47 @@ class PluginManager:
|
||||
if base == sf_id or base == sf_id.replace("sf_", ""):
|
||||
path = os.path.join(d, f)
|
||||
try:
|
||||
import fluidsynth
|
||||
fl = fluidsynth.Synth()
|
||||
fid = fl.sfload(path)
|
||||
if fid < 0:
|
||||
fl.delete()
|
||||
continue
|
||||
presets = []
|
||||
_fl = fluidsynth._fl
|
||||
_fl.fluid_synth_get_sfont_by_id.restype = c_void_p
|
||||
_fl.fluid_preset_get_name.restype = c_char_p
|
||||
_fl.fluid_sfont_get_preset.restype = c_void_p
|
||||
sfont_ptr = _fl.fluid_synth_get_sfont_by_id(c_void_p(fl.synth), c_int(fid))
|
||||
if sfont_ptr:
|
||||
for bank in range(0, 2):
|
||||
for prog_num in range(0, 128):
|
||||
try:
|
||||
preset = fluidsynth.fluid_sfont_get_preset(sfont_ptr, c_int(bank), c_int(prog_num))
|
||||
except Exception:
|
||||
break
|
||||
if preset:
|
||||
name_ptr = fluidsynth.fluid_preset_get_name(preset)
|
||||
if name_ptr:
|
||||
name_val = c_char_p(name_ptr).value
|
||||
if name_val:
|
||||
presets.append({
|
||||
"bank": bank,
|
||||
"program": prog_num,
|
||||
"name": name_val.decode("utf-8", errors="replace")
|
||||
})
|
||||
fl.delete()
|
||||
_SF_INSTRUMENTS_CACHE[sf_id] = presets[:256]
|
||||
return presets[:256]
|
||||
import fluidsynth as _fs
|
||||
# Low-level CFFI API (same as render_engine); never use
|
||||
# the high-level Synth() class that this binding lacks.
|
||||
_settings = _fs.new_fluid_settings()
|
||||
_synth = _fs.new_fluid_synth(_settings)
|
||||
try:
|
||||
fid = _fs.fluid_synth_sfload(_synth, path.encode("utf-8"), 1)
|
||||
if fid < 0:
|
||||
continue
|
||||
sfont = _fs.fluid_synth_get_sfont_by_id(_synth, fid)
|
||||
presets = []
|
||||
if sfont:
|
||||
for bank in range(0, 2):
|
||||
for prog_num in range(0, 128):
|
||||
try:
|
||||
preset = _fs.fluid_sfont_get_preset(sfont, bank, prog_num)
|
||||
except Exception:
|
||||
break
|
||||
if preset:
|
||||
try:
|
||||
name_ptr = _fs.fluid_preset_get_name(preset)
|
||||
if name_ptr:
|
||||
if hasattr(_fs, "ffi"):
|
||||
raw = _fs.ffi.string(name_ptr)
|
||||
else:
|
||||
raw = c_char_p(name_ptr).value
|
||||
if raw:
|
||||
presets.append({
|
||||
"bank": bank,
|
||||
"program": prog_num,
|
||||
"name": raw.decode("utf-8", errors="replace")
|
||||
})
|
||||
except Exception:
|
||||
continue
|
||||
_SF_INSTRUMENTS_CACHE[sf_id] = presets[:256]
|
||||
return presets[:256]
|
||||
finally:
|
||||
try:
|
||||
_fs.delete_fluid_synth(_synth)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
import traceback; traceback.print_exc()
|
||||
_SF_INSTRUMENTS_CACHE[sf_id] = []
|
||||
@@ -285,8 +318,9 @@ class PluginManager:
|
||||
def list_available(self) -> dict:
|
||||
return {
|
||||
"vst_instruments": [
|
||||
{"id": k, "name": k, "type": "VST3", "has_native_support": HAS_PEDALBOARD}
|
||||
for k in self._scan_plugins().keys()
|
||||
{"id": "vst3_" + k.lower().replace(" ", "_"), "name": k, "type": "VST3",
|
||||
"path": p, "has_native_support": HAS_PEDALBOARD}
|
||||
for k, p in self._scan_plugins().items()
|
||||
],
|
||||
"soundfonts": self._scan_soundfonts()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import os
|
||||
import platform
|
||||
import logging
|
||||
from typing import List, Dict, Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NativePluginScanner:
|
||||
"""Quét thư mục custom chứa VST3 (.vst3) / Audio Unit (.component trên macOS)
|
||||
— spec DESKTOP VST (2026-08-08): POST /api/v1/desktop/plugins/scan."""
|
||||
|
||||
def __init__(self):
|
||||
self.os_type = platform.system() # 'Linux', 'Windows', 'Darwin'
|
||||
|
||||
def scan_directories(self, target_paths: List[str]) -> List[Dict[str, Any]]:
|
||||
found_plugins = []
|
||||
seen = set()
|
||||
|
||||
for base_path in target_paths:
|
||||
if not base_path:
|
||||
continue
|
||||
if not os.path.exists(base_path):
|
||||
logger.warning(f"Scan path does not exist: {base_path}")
|
||||
continue
|
||||
|
||||
for root, dirs, files in os.walk(base_path):
|
||||
for dir_name in dirs:
|
||||
full_path = os.path.join(root, dir_name)
|
||||
|
||||
# VST3 = bundle directory *.vst3
|
||||
if dir_name.endswith('.vst3'):
|
||||
plugin_name = dir_name[:-5]
|
||||
pid = f"vst3_{plugin_name.lower().replace(' ', '_')}"
|
||||
if pid in seen:
|
||||
continue
|
||||
seen.add(pid)
|
||||
found_plugins.append({
|
||||
"id": pid,
|
||||
"name": plugin_name,
|
||||
"type": "VST3",
|
||||
"format": "VST3",
|
||||
"category": "Instrument",
|
||||
"vendor": self._read_vendor(full_path) or None,
|
||||
"path": full_path,
|
||||
"is_instrument": True,
|
||||
"has_native_gui": True
|
||||
})
|
||||
# Audio Unit = bundle *.component (macOS only)
|
||||
elif dir_name.endswith('.component') and self.os_type == 'Darwin':
|
||||
plugin_name = dir_name[:-10]
|
||||
pid = f"au_{plugin_name.lower().replace(' ', '_')}"
|
||||
if pid in seen:
|
||||
continue
|
||||
seen.add(pid)
|
||||
found_plugins.append({
|
||||
"id": pid,
|
||||
"name": plugin_name,
|
||||
"type": "AU",
|
||||
"format": "AudioUnit",
|
||||
"category": "Instrument",
|
||||
"vendor": self._read_vendor(full_path) or None,
|
||||
"path": full_path,
|
||||
"is_instrument": True,
|
||||
"has_native_gui": True
|
||||
})
|
||||
|
||||
return found_plugins
|
||||
|
||||
@staticmethod
|
||||
def _read_vendor(bundle_path: str) -> str:
|
||||
"""Đọc vendor từ Info.plist nếu có (macOS) — best-effort."""
|
||||
try:
|
||||
plist = os.path.join(bundle_path, "Contents", "Info.plist")
|
||||
if os.path.exists(plist):
|
||||
with open(plist, "rb") as f:
|
||||
raw = f.read(65536)
|
||||
import plistlib
|
||||
try:
|
||||
data = plistlib.loads(raw)
|
||||
return data.get("CFBundleManufacturer") or data.get("NSHumanReadableCopyright")
|
||||
except Exception:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
+38
-26
@@ -1,8 +1,11 @@
|
||||
import os, threading
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.responses import HTMLResponse, FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
from app.config import settings
|
||||
from app.api.v1.audio import router as audio_router
|
||||
from app.api.v1.tasks import router as tasks_router
|
||||
@@ -14,24 +17,41 @@ from app.api.v1.user_config import router as user_config_router
|
||||
from app.api.v1.ai_proxy import router as ai_proxy_router
|
||||
from app.api.v1.ai_presets import router as ai_presets_router
|
||||
from app.api.v1.plugins import router as plugins_router
|
||||
from app.api.v1.desktop_plugins import router as desktop_plugins_router
|
||||
from app.api.v1.media import router as media_router
|
||||
from app.core.auth import seed_admin
|
||||
from app.core.soundfont_converter import SoundFontConverter
|
||||
from app.core.soundfont_scanner import SoundFontAutoScanner
|
||||
|
||||
# Ensure storage directories exist
|
||||
os.makedirs(settings.UPLOADS_DIR, exist_ok=True)
|
||||
os.makedirs(settings.PROCESSED_DIR, exist_ok=True)
|
||||
|
||||
app = FastAPI(title="SonicForge API Engine")
|
||||
_SF_SCANNER_STOP = None
|
||||
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Startup
|
||||
seed_admin()
|
||||
scanner = SoundFontAutoScanner()
|
||||
global _SF_SCANNER_STOP
|
||||
_SF_SCANNER_STOP = scanner.start_background(interval=30)
|
||||
yield
|
||||
# Shutdown
|
||||
if _SF_SCANNER_STOP is not None:
|
||||
_SF_SCANNER_STOP.set()
|
||||
|
||||
|
||||
app = FastAPI(title="SonicForge API Engine", lifespan=lifespan)
|
||||
|
||||
app.add_middleware(GZipMiddleware, minimum_size=500)
|
||||
|
||||
# Auth is token/cookie based (no cookies required for CORS), so credentials are
|
||||
# disabled — "*" + allow_credentials=True is rejected by browsers anyway.
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_credentials=False,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
@@ -53,25 +73,13 @@ app.include_router(user_config_router, prefix="/api/v1/user", tags=["user_config
|
||||
app.include_router(ai_proxy_router, prefix="/api/v1/ai", tags=["ai"])
|
||||
app.include_router(ai_presets_router, prefix="/api/v1/ai", tags=["ai"])
|
||||
app.include_router(plugins_router, prefix="/api/v1/plugins", tags=["plugins"])
|
||||
app.include_router(desktop_plugins_router, prefix="/api/v1/desktop", tags=["desktop"])
|
||||
app.include_router(media_router, prefix="/api/v1/media", tags=["media"])
|
||||
|
||||
# Seed admin user on startup
|
||||
@app.on_event("startup")
|
||||
async def startup_seed_admin():
|
||||
seed_admin()
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_convert_soundfonts():
|
||||
def _run():
|
||||
try:
|
||||
SoundFontConverter().batch_convert_all()
|
||||
except Exception as e:
|
||||
print(f"[Startup] SoundFont conversion error: {e}")
|
||||
threading.Thread(target=_run, daemon=True).start()
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_sf_scanner():
|
||||
scanner = SoundFontAutoScanner()
|
||||
scanner.start_background(interval=30)
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def get_index():
|
||||
@@ -79,14 +87,18 @@ async def get_index():
|
||||
if not os.path.exists(index_path):
|
||||
return HTMLResponse(content=f"<h1>SonicForge Studio: index.html not found at {index_path}</h1>", status_code=404)
|
||||
with open(index_path, "r", encoding="utf-8") as file:
|
||||
return HTMLResponse(content=file.read(), status_code=200)
|
||||
resp = HTMLResponse(content=file.read(), status_code=200)
|
||||
# no-cache: index.html PHẢI luôn mới (các bundle JS dùng ?v= để bust) —
|
||||
# nếu browser cache HTML cũ → stamp cũ → tải bundle cũ (bug "không load
|
||||
# được bundle mới" ở incognito — cache heuristic không có Cache-Control).
|
||||
resp.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
||||
return resp
|
||||
|
||||
|
||||
@app.get("/favicon.svg")
|
||||
async def get_favicon():
|
||||
import os
|
||||
favicon_path = os.path.join(settings.TEMPLATES_DIR, "favicon.svg")
|
||||
if os.path.exists(favicon_path):
|
||||
from fastapi.responses import FileResponse
|
||||
return FileResponse(favicon_path, media_type="image/svg+xml")
|
||||
return HTMLResponse(content="", status_code=404)
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"id": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"type": { "type": "string", "enum": ["AUDIO", "MIDI", "SECTION"] },
|
||||
"color": { "type": ["string", "null"], "default": null },
|
||||
"volume_db": { "type": "number", "default": 0.0 },
|
||||
"pan": { "type": "number", "minimum": -1.0, "maximum": 1.0, "default": 0.0 },
|
||||
"mute": { "type": "boolean", "default": false },
|
||||
|
||||
+17
-1
@@ -5,12 +5,18 @@ import time
|
||||
from typing import Optional, Dict, Any, List
|
||||
from app.config import settings
|
||||
|
||||
DB_PATH = os.path.join(settings.STORAGE_DIR, "sonicforge.db")
|
||||
# Default DB lives in storage/; tests override via SONICFORGE_DB_PATH so the
|
||||
# dev database is never touched by the test suite.
|
||||
DB_PATH = os.getenv("SONICFORGE_DB_PATH") or os.path.join(settings.STORAGE_DIR, "sonicforge.db")
|
||||
|
||||
def get_db_connection():
|
||||
os.makedirs(settings.STORAGE_DIR, exist_ok=True)
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
# WAL improves concurrent read/write; FK enforcement makes quota/backup
|
||||
# cleanup consistent when users are deleted.
|
||||
conn.execute("PRAGMA journal_mode=WAL")
|
||||
conn.execute("PRAGMA foreign_keys=ON")
|
||||
return conn
|
||||
|
||||
def init_db():
|
||||
@@ -89,6 +95,16 @@ def init_db():
|
||||
);
|
||||
""")
|
||||
|
||||
# Placeholder user for anonymous autosave: projects are saved with
|
||||
# user_id='anonymous' when no token is present, so the FK must resolve.
|
||||
cursor.execute("SELECT id FROM users WHERE id = 'anonymous'")
|
||||
if not cursor.fetchone():
|
||||
import secrets as _secrets
|
||||
cursor.execute("""
|
||||
INSERT OR IGNORE INTO users (id, username, email, hashed_password, role, must_change_password, created_at, is_active)
|
||||
VALUES ('anonymous', 'anonymous', 'anonymous@local', ?, 'standard', 0, ?, 0)
|
||||
""", (_secrets.token_hex(32), time.time()))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
+8819
-888
File diff suppressed because it is too large
Load Diff
+1242
-157
File diff suppressed because one or more lines are too long
@@ -121,6 +121,57 @@ const AIGateway = (function() {
|
||||
}
|
||||
};
|
||||
|
||||
// ai_midi_rearrange_specification.md §3 — Function Tool Schema hỗ trợ 2 mode:
|
||||
// SIMILAR_VARIATION (biến tấu cùng độ dài) / EXTEND_CONTINUATION (viết tiếp
|
||||
// các bar sau). AI trả gói dữ liệu có vị trí target trên Timeline.
|
||||
const REARRANGE_EXTEND_TOOL_SPEC = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'rearrange_or_extend_midi_melody',
|
||||
description: 'Analyzes source MIDI melody data and returns either a variation (Variation) or continuation (Extend) based on user instructions.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
mode: {
|
||||
type: 'string',
|
||||
enum: ['SIMILAR_VARIATION', 'EXTEND_CONTINUATION'],
|
||||
description: "Mode: 'SIMILAR_VARIATION' (new arrangement of equal length) or 'EXTEND_CONTINUATION' (writes subsequent bars)."
|
||||
},
|
||||
composition_title: {
|
||||
type: 'string',
|
||||
description: 'Short title describing the new melody style (e.g., Jazz Swing Variation, Epic Extension Part 2)'
|
||||
},
|
||||
target_start_bar: {
|
||||
type: 'number',
|
||||
description: 'Starting bar number for the generated notes on the Timeline'
|
||||
},
|
||||
target_duration_bars: {
|
||||
type: 'number',
|
||||
description: 'Total bar duration covered by the generated sequence'
|
||||
},
|
||||
soundfont_id: { type: 'string', default: 'generaluser_gs' },
|
||||
soundfont_bank: { type: 'integer', default: 0 },
|
||||
soundfont_program: { type: 'integer', default: 0 },
|
||||
generated_notes: {
|
||||
type: 'array',
|
||||
description: 'Array of AI-generated MIDI notes.',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
pitch: { type: 'integer', minimum: 0, maximum: 127 },
|
||||
start_beat: { type: 'number', description: 'Starting beat position relative to beat 0.0 of the generated item' },
|
||||
duration_beats: { type: 'number', minimum: 0.1 },
|
||||
velocity: { type: 'number', minimum: 0.0, maximum: 1.0 }
|
||||
},
|
||||
required: ['pitch', 'start_beat', 'duration_beats', 'velocity']
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['mode', 'composition_title', 'target_start_bar', 'target_duration_bars', 'generated_notes']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const REARRANGE_SCENARIOS = [
|
||||
{
|
||||
id: 'arpeggio',
|
||||
@@ -275,7 +326,10 @@ ${rules.join('\n')}` },
|
||||
} else {
|
||||
response = await fetch(`${origin}/api/v1/ai/proxy`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(localStorage.getItem('sonic_token') ? { 'X-Auth-Token': localStorage.getItem('sonic_token') } : {})
|
||||
},
|
||||
body: JSON.stringify({ url, headers, body })
|
||||
});
|
||||
}
|
||||
@@ -453,6 +507,7 @@ Ví dụ: "Hãy chọn và copy từ bar 4 đến bar 12 của track 1 sau đó
|
||||
return {
|
||||
DEFAULT_TOOLS,
|
||||
REARRANGE_TOOL_SPEC,
|
||||
REARRANGE_EXTEND_TOOL_SPEC,
|
||||
REARRANGE_SCENARIOS,
|
||||
detectRearrangeScenario,
|
||||
buildRearrangeMessage,
|
||||
|
||||
@@ -22,6 +22,8 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem('sonic_token');
|
||||
localStorage.removeItem('sonic_user');
|
||||
// Mở màn hình đăng nhập ngay (bug 09:45 — màn đen khi session hết hạn)
|
||||
if (window.__onAuthFail) window.__onAuthFail();
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => ({}));
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
if (!track.midiItems || !track.midiItems.length) continue;
|
||||
if (track.muted) continue;
|
||||
|
||||
var isSameTrack = track.id === targetTrackId;
|
||||
var trackGhostNotes = [];
|
||||
|
||||
for (var j = 0; j < track.midiItems.length; j++) {
|
||||
@@ -32,7 +33,10 @@
|
||||
var itemStartBeat = item.startTime / secondsPerBeat;
|
||||
var itemEndBeat = (item.startTime + item.duration) / secondsPerBeat;
|
||||
|
||||
if (itemStartBeat >= windowEndBeat || itemEndBeat < windowStartBeat) continue;
|
||||
// Same-track items always contribute notes (show whole track); cross-track only when overlapping the window
|
||||
if (itemStartBeat >= windowEndBeat || itemEndBeat < windowStartBeat) {
|
||||
if (!isSameTrack) continue;
|
||||
}
|
||||
|
||||
var notes = item.notes || [];
|
||||
for (var k = 0; k < notes.length; k++) {
|
||||
@@ -40,7 +44,7 @@
|
||||
var noteAbsStart = itemStartBeat + (note.start_beat || 0);
|
||||
var noteAbsEnd = noteAbsStart + (note.duration_beats || 1);
|
||||
|
||||
if (noteAbsStart >= windowEndBeat) continue;
|
||||
if (noteAbsStart >= windowEndBeat && !isSameTrack) continue;
|
||||
|
||||
trackGhostNotes.push({
|
||||
id: 'ghost_' + (note.id || Math.random().toString(36).substr(2, 9)),
|
||||
@@ -48,6 +52,7 @@
|
||||
relative_start_beat: noteAbsStart - windowStartBeat,
|
||||
duration_beats: (note.duration_beats || 1),
|
||||
velocity: note.velocity,
|
||||
item_id: item.id,
|
||||
original_track_name: track.name,
|
||||
original_track_color: track.color || '#888888'
|
||||
});
|
||||
@@ -59,6 +64,7 @@
|
||||
track_id: track.id,
|
||||
track_name: track.name,
|
||||
track_color: track.color || '#6b7280',
|
||||
isSameTrack: isSameTrack,
|
||||
notes: trackGhostNotes
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
(function () {
|
||||
const RENDER_BLOCK = 512;
|
||||
const QUEUE_TARGET = 4;
|
||||
const QUEUE_TARGET = 16;
|
||||
let _audioCtx = null;
|
||||
let _fluidModule = null;
|
||||
let _synthPtr = null;
|
||||
@@ -22,14 +22,21 @@
|
||||
let _loadedFonts = {};
|
||||
let _activeOscillators = {};
|
||||
let _gainNode = null;
|
||||
let _pendingOutputDestination = null;
|
||||
let _outputDestination = null; // cache đích route — dedupe swap dư giữa stream
|
||||
let _validPercCache = {}; // { sfId: [bank, prog] | null } — preset percussion hợp lệ
|
||||
let _sfLoadFailAt = {}; // { sfId: timestamp } — cooldown 10s sau load fail
|
||||
let _scheduledNotes = [];
|
||||
let _loadPromises = {};
|
||||
let _sfloadSeq = 0;
|
||||
|
||||
const getCtx = function () {
|
||||
if (_audioCtx) {
|
||||
if (!_gainNode) {
|
||||
_gainNode = _audioCtx.createGain();
|
||||
_gainNode.gain.value = 0.3;
|
||||
_gainNode.connect(window.masterBus ? window.masterBus.input : _audioCtx.destination);
|
||||
_outputDestination = _pendingOutputDestination || (window.masterBus ? window.masterBus.input : _audioCtx.destination);
|
||||
_gainNode.connect(_outputDestination);
|
||||
}
|
||||
return _audioCtx;
|
||||
}
|
||||
@@ -38,7 +45,8 @@
|
||||
if (!_gainNode) {
|
||||
_gainNode = ctx.createGain();
|
||||
_gainNode.gain.value = 0.3;
|
||||
_gainNode.connect(window.masterBus ? window.masterBus.input : ctx.destination);
|
||||
_outputDestination = _pendingOutputDestination || (window.masterBus ? window.masterBus.input : ctx.destination);
|
||||
_gainNode.connect(_outputDestination);
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
@@ -51,7 +59,8 @@
|
||||
if (!_gainNode) {
|
||||
_gainNode = window.__sharedAudioCtx.createGain();
|
||||
_gainNode.gain.value = 0.3;
|
||||
_gainNode.connect(window.__sharedAudioCtx.destination);
|
||||
_outputDestination = _pendingOutputDestination || window.__sharedAudioCtx.destination;
|
||||
_gainNode.connect(_outputDestination);
|
||||
}
|
||||
return window.__sharedAudioCtx;
|
||||
};
|
||||
@@ -59,6 +68,41 @@
|
||||
const SonicSF = {
|
||||
loadedFonts: _loadedFonts,
|
||||
|
||||
// Route the shared FluidSynth output through a per-track node (e.g. the
|
||||
// track's gainNode) so the track's FX chain / fader / pan affect the
|
||||
// soundfont instrument. Pass null to restore the default master-bus route.
|
||||
setOutputDestination: function (node) {
|
||||
try {
|
||||
if (_gainNode) {
|
||||
const dest = node || (window.masterBus ? window.masterBus.input : ((_audioCtx || window.__sharedAudioCtx).destination));
|
||||
// DEDUPE: đích không đổi → KHÔNG disconnect/reconnect.
|
||||
// Swap dư giữa dòng notes đang phát (applyAllTrackMuteSolo →
|
||||
// updateSfRouting gọi lại cùng đích sfEntry sau noteon đầu)
|
||||
// làm ScriptProcessor xuất buffer uninitialized → NaN →
|
||||
// 11 biquad "state is bad" → CÂM (mọi log: state-bad nổ
|
||||
// ngay sau setOutputDestination lần 2).
|
||||
if (dest === _outputDestination) return;
|
||||
_gainNode.disconnect();
|
||||
_gainNode.connect(dest);
|
||||
// Reconnect VU analyser tap (app.jsx VU tick gắn __vuAnalyser
|
||||
// trên _gainNode): disconnect() không đối số ngắt MỌI kết
|
||||
// nối kể cả analyser → sfAudioPeak = 0 mãi → track VU MIDI
|
||||
// không nhảy khi có âm (user bug 06:35).
|
||||
if (_gainNode.__vuAnalyser) _gainNode.connect(_gainNode.__vuAnalyser);
|
||||
_outputDestination = dest;
|
||||
console.log('[SonicSF] setOutputDestination to:', node ? 'track node (sfEntry)' : 'masterBus.input');
|
||||
} else {
|
||||
_pendingOutputDestination = node || null;
|
||||
console.log('[SonicSF] setOutputDestination pending:', node ? 'track node (sfEntry)' : 'null');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[SonicSF] setOutputDestination error:', e);
|
||||
}
|
||||
},
|
||||
getOutputNode: function () {
|
||||
return _gainNode;
|
||||
},
|
||||
|
||||
init: async function (audioContext) {
|
||||
if (_initialized && _fluidModule) return;
|
||||
if (_initPromise) return _initPromise;
|
||||
@@ -79,19 +123,27 @@
|
||||
if (!_gainNode) {
|
||||
_gainNode = _audioCtx.createGain();
|
||||
_gainNode.gain.value = 0.3;
|
||||
_gainNode.connect(window.masterBus ? window.masterBus.input : _audioCtx.destination);
|
||||
_outputDestination = _pendingOutputDestination || (window.masterBus ? window.masterBus.input : _audioCtx.destination);
|
||||
_gainNode.connect(_outputDestination);
|
||||
}
|
||||
|
||||
console.log("[SonicSF] AudioCtx state:", _audioCtx.state, "sampleRate:", _audioCtx.sampleRate);
|
||||
|
||||
// ── Renderer selection ──
|
||||
// ScriptProcessor is the default and FINAL choice: it is
|
||||
// pull-based (onaudioprocess is invoked by the audio thread),
|
||||
// so it cannot starve when the main thread is busy (font
|
||||
// loading, WASM decode, UI). The AudioWorklet push model
|
||||
// (setInterval on the main thread) starves under load and
|
||||
// produces SILENCE on this machine — repeatedly confirmed.
|
||||
// The deprecation console warning is purely cosmetic.
|
||||
var _useScriptNode = true;
|
||||
try {
|
||||
await _audioCtx.audioWorklet.addModule('/static/js/worklets/fluidsynth-bridge.js');
|
||||
console.log("[SonicSF] Worklet registered OK");
|
||||
await _audioCtx.audioWorklet.addModule('/static/js/worklets/fluidsynth-bridge.js?v=202608031240');
|
||||
console.log("[SonicSF] Worklet registered OK (unused)");
|
||||
} catch (e) {
|
||||
console.warn("[SonicSF] Worklet reg failed:", e);
|
||||
}
|
||||
console.log("[SonicSF] Using ScriptProcessorNode (forced for debug)");
|
||||
|
||||
console.log("[SonicSF] Initializing FluidSynth WASM Engine...");
|
||||
var TOTAL_MEMORY = 256 * 1024 * 1024;
|
||||
@@ -104,6 +156,11 @@
|
||||
},
|
||||
TOTAL_MEMORY: TOTAL_MEMORY,
|
||||
printErr: function (msg) {
|
||||
// "No preset found on channel" is FluidSynth's
|
||||
// expected notice when a soundfont simply has no
|
||||
// preset for a bank (e.g. bank 128 on a melodic-only
|
||||
// font) — the note is just silent, not an error.
|
||||
if (msg && (msg.indexOf('No preset found on channel') !== -1 || msg.indexOf('There is no preset with bank number') !== -1)) return;
|
||||
console.warn('[FluidSynth:err]', msg);
|
||||
}
|
||||
});
|
||||
@@ -114,9 +171,9 @@
|
||||
|
||||
_settingsPtr = _fluidModule._new_fluid_settings();
|
||||
_fluidModule._fluid_settings_setnum(_settingsPtr, "synth.sample-rate", _audioCtx.sampleRate || 44100);
|
||||
_fluidModule._fluid_settings_setnum(_settingsPtr, "synth.gain", 2.0);
|
||||
_fluidModule._fluid_settings_setnum(_settingsPtr, "synth.gain", 1.0);
|
||||
_fluidModule._fluid_settings_setnum(_settingsPtr, "synth.polyphony", 256);
|
||||
_fluidModule._fluid_settings_setint(_settingsPtr, "synth.verbose", 1);
|
||||
_fluidModule._fluid_settings_setint(_settingsPtr, "synth.verbose", 0);
|
||||
_fluidModule._fluid_settings_setint(_settingsPtr, "synth.ladspa.active", 0);
|
||||
_fluidModule._fluid_settings_setstr(_settingsPtr, "player.timing-source", "audio");
|
||||
console.log("[SonicSF] FluidSynth settings configured");
|
||||
@@ -132,7 +189,15 @@
|
||||
|
||||
if (!_useScriptNode) {
|
||||
try {
|
||||
_workletNode = new AudioWorkletNode(_audioCtx, 'fluidsynth-bridge');
|
||||
// Force stereo output regardless of the device's
|
||||
// channel count — FluidSynth renders stereo, and a
|
||||
// mono output would crash the worklet (out[1] undefined).
|
||||
_workletNode = new AudioWorkletNode(_audioCtx, 'fluidsynth-bridge', {
|
||||
numberOfOutputs: 1,
|
||||
outputChannelCount: [2],
|
||||
channelCount: 2,
|
||||
channelCountMode: 'explicit'
|
||||
});
|
||||
_workletNode.connect(_gainNode);
|
||||
console.log("[SonicSF] AudioWorklet node connected via gain");
|
||||
_startRenderLoop();
|
||||
@@ -146,17 +211,31 @@
|
||||
var spn = _audioCtx.createScriptProcessor(spBufSz, 0, 2);
|
||||
var lp = _fluidModule._malloc(spBufSz * 4);
|
||||
var rp = _fluidModule._malloc(spBufSz * 4);
|
||||
// Heap WASM có thể realloc khi load SoundFont lớn (SGM-V2.01
|
||||
// ~300MB) → lp/rp DANGLE → đọc vùng nhớ đã free → NaN/garbage
|
||||
// → master chain "state is bad" → CÂM + stuck. Theo dõi
|
||||
// buffer + re-malloc khi đổi.
|
||||
var _heapBufRef = _fluidModule.HEAPU8.buffer;
|
||||
spn.onaudioprocess = function (e) {
|
||||
var left = e.outputBuffer.getChannelData(0);
|
||||
var right = e.outputBuffer.getChannelData(1);
|
||||
var sz = left.length;
|
||||
try {
|
||||
if (_fluidModule.HEAPU8.buffer !== _heapBufRef) {
|
||||
try { _fluidModule._free(lp); _fluidModule._free(rp); } catch (er2) {}
|
||||
lp = _fluidModule._malloc(sz * 4);
|
||||
rp = _fluidModule._malloc(sz * 4);
|
||||
_heapBufRef = _fluidModule.HEAPU8.buffer;
|
||||
}
|
||||
_fluidModule._fluid_synth_write_float(_synthPtr, sz, lp, 0, 1, rp, 0, 1);
|
||||
var hf = _fluidModule.HEAPF32;
|
||||
var lpb = lp >> 2, rpb = rp >> 2;
|
||||
for (var si = 0; si < sz; si++) {
|
||||
left[si] = hf[lpb + si];
|
||||
right[si] = hf[rpb + si];
|
||||
// NaN sweep: mẫu NaN/Inf → 0 (chain biquad
|
||||
// KHÔNG BAO GIỜ được nhận NaN → không state-bad).
|
||||
var L = hf[lpb + si], R = hf[rpb + si];
|
||||
left[si] = isFinite(L) ? L : 0;
|
||||
right[si] = isFinite(R) ? R : 0;
|
||||
}
|
||||
} catch (er) {}
|
||||
};
|
||||
@@ -186,11 +265,16 @@
|
||||
},
|
||||
|
||||
_tryLoadSFL: function (buf, ext) {
|
||||
var fname = '/' + ext + '_' + Date.now();
|
||||
var fname = '/' + ext + '_' + (++_sfloadSeq) + '_' + Date.now();
|
||||
try { _fluidModule.FS.unlink(fname); } catch (e) {}
|
||||
_fluidModule.FS.writeFile(fname, new Uint8Array(buf));
|
||||
var cPath = this._allocCStr(fname);
|
||||
var handle = _fluidModule._fluid_synth_sfload(_synthPtr, cPath, 1);
|
||||
// reset_presets = 0: loading a NEW soundfont must NOT reset the
|
||||
// presets already selected on other channels. With 1, FluidSynth
|
||||
// re-points every channel to the new font's preset 0, so loading a
|
||||
// second instrument silently changes the first one's sound
|
||||
// (decay/loop envelope…).
|
||||
var handle = _fluidModule._fluid_synth_sfload(_synthPtr, cPath, 0);
|
||||
_fluidModule._free(cPath);
|
||||
try { _fluidModule.FS.unlink(fname); } catch (e) {}
|
||||
return handle;
|
||||
@@ -203,19 +287,63 @@
|
||||
_currentSfId = sfId;
|
||||
return true;
|
||||
}
|
||||
// Deduplicate concurrent loads: rapid key presses (or several armed
|
||||
// tracks) all call loadSoundFont for the same font before the first
|
||||
// load resolves. Without this, the same soundfont is sfload'd several
|
||||
// times (handles 1,2,3,4…) — wasting the 256MB WASM heap and stalling
|
||||
// notes until each load finishes (audible lag, then silence).
|
||||
if (!_loadPromises[sfId]) {
|
||||
_loadPromises[sfId] = this._doLoadSoundFont(sfId).then(function (ok) {
|
||||
// Do NOT cache failures: a transient error (network hiccup,
|
||||
// memory pressure) must not permanently kill the instrument —
|
||||
// the next note retries the load and recovers.
|
||||
if (!ok) delete _loadPromises[sfId];
|
||||
return ok;
|
||||
});
|
||||
}
|
||||
return _loadPromises[sfId];
|
||||
},
|
||||
|
||||
_doLoadSoundFont: async function (sfId) {
|
||||
try {
|
||||
// KHÔNG unload SF cũ khi sfload SF mới: unload làm handle cũ
|
||||
// thành rác trong khi channel state vẫn trỏ tới → program_select
|
||||
// bị skip (progAlreadySet) → noteon trên handle đã unload →
|
||||
// "Instrument not found ... substituted prog 0". Heap 256MB đủ
|
||||
// cho vài SF (SGM + latin = 2 handle — log OK). SF cũ khi cần
|
||||
// lại chỉ được sfload lại nếu map bị xóa (không xảy ra ở đây).
|
||||
var cache = window.SonicSFStorage;
|
||||
var buf = cache ? await cache.getBuffer(sfId) : null;
|
||||
if (!buf) {
|
||||
var url = "/api/v1/plugins/soundfonts/download/" + encodeURIComponent(sfId) + "?t=" + Date.now();
|
||||
var resp = await fetch(url);
|
||||
if (!resp.ok) {
|
||||
if (buf) {
|
||||
var cachedOk = this._tryLoadSFL(buf, '.sf3');
|
||||
if (cachedOk === -1) cachedOk = this._tryLoadSFL(buf, '.sf2');
|
||||
if (cachedOk !== -1) {
|
||||
_sfHandleMap.set(sfId, cachedOk);
|
||||
_currentSfId = sfId;
|
||||
_loadedFonts[sfId] = true;
|
||||
console.log("[SonicSF] SoundFont loaded from cache:", sfId, "handle:", cachedOk);
|
||||
return true;
|
||||
}
|
||||
// Stale/corrupt cache (e.g. old SF3 buffers the WASM can't
|
||||
// decode) — drop it and re-download from the server.
|
||||
console.warn("[SonicSF] Cached SoundFont unplayable, re-downloading:", sfId);
|
||||
try { await cache.saveBuffer(sfId, null); } catch (e2) {}
|
||||
}
|
||||
var url = "/api/v1/plugins/soundfonts/download/" + encodeURIComponent(sfId) + "?t=" + Date.now();
|
||||
var resp = await fetch(url);
|
||||
if (!resp.ok) {
|
||||
// Fallback: font bundled theo deployment (static/soundfonts —
|
||||
// serve qua /soundfonts/{f} — catalog default-soundfonts).
|
||||
var url2 = "/soundfonts/" + encodeURIComponent(sfId.replace(/^sf_/, '')) + "?t=" + Date.now();
|
||||
var resp2 = await fetch(url2);
|
||||
if (!resp2.ok) {
|
||||
console.warn("[SonicSF] SoundFont not found:", sfId);
|
||||
return false;
|
||||
}
|
||||
buf = await resp.arrayBuffer();
|
||||
if (cache) await cache.saveBuffer(sfId, buf);
|
||||
resp = resp2;
|
||||
}
|
||||
buf = await resp.arrayBuffer();
|
||||
if (cache) await cache.saveBuffer(sfId, buf);
|
||||
var sfHandle = this._tryLoadSFL(buf, '.sf3');
|
||||
if (sfHandle === -1) {
|
||||
console.warn("[SonicSF] sfload .sf3 failed, trying .sf2 for", sfId);
|
||||
@@ -245,16 +373,16 @@
|
||||
if (!ok) return;
|
||||
}
|
||||
var engKey = (sfId || '') + ':' + bank + ':' + program;
|
||||
if (!_engineChMap[engKey]) {
|
||||
if (channel === undefined || channel === null) {
|
||||
if (channel === undefined || channel === null) {
|
||||
if (!_engineChMap[engKey]) {
|
||||
var allocCh = this.allocateChannel(bank);
|
||||
_engineChMap[engKey] = allocCh;
|
||||
channel = allocCh;
|
||||
} else {
|
||||
_engineChMap[engKey] = channel;
|
||||
channel = _engineChMap[engKey];
|
||||
}
|
||||
} else {
|
||||
channel = _engineChMap[engKey];
|
||||
} else if (!_engineChMap[engKey]) {
|
||||
_engineChMap[engKey] = channel;
|
||||
}
|
||||
var sfHandle = _sfHandleMap.get(sfId);
|
||||
if (sfHandle !== undefined) {
|
||||
@@ -371,10 +499,11 @@
|
||||
},
|
||||
|
||||
_playNoteFluid: function (note, velocity, durationMs, startTime, program, channel, synthEngine) {
|
||||
var midiPitch = Math.min(127, Math.max(0, parseInt(note) || 60));
|
||||
var midiVel = Math.min(127, Math.max(1, Math.floor(
|
||||
typeof velocity === 'number' ? (velocity > 1 ? velocity : velocity * 127) : 100
|
||||
)));
|
||||
var parsedPitch = parseInt(note);
|
||||
var midiPitch = isNaN(parsedPitch) ? 60 : Math.min(127, Math.max(0, parsedPitch));
|
||||
var rawVel = (typeof velocity === 'number' && isFinite(velocity)) ? (velocity > 1 ? velocity : velocity * 127) : 100;
|
||||
if (isNaN(rawVel)) rawVel = 100;
|
||||
var midiVel = Math.min(127, Math.max(1, Math.floor(rawVel)));
|
||||
var _origChannel = channel;
|
||||
var usedBank = 0, usedProg = 0;
|
||||
if (synthEngine) {
|
||||
@@ -407,30 +536,99 @@
|
||||
var self = this;
|
||||
var doNote = function () {
|
||||
try {
|
||||
var finalBank = usedBank;
|
||||
var finalProg = usedProg;
|
||||
var finalBank = parseInt(usedBank);
|
||||
if (isNaN(finalBank) || !isFinite(finalBank)) finalBank = 0;
|
||||
var finalProg = parseInt(usedProg);
|
||||
if (isNaN(finalProg) || !isFinite(finalProg)) finalProg = 0;
|
||||
var finalSfId = synthEngine ? synthEngine.soundfont_id : undefined;
|
||||
if (_channels[ch] && _channels[ch].program !== undefined) {
|
||||
finalBank = _channels[ch].bank;
|
||||
finalProg = _channels[ch].program;
|
||||
if (_channels[ch].sfId !== undefined) {
|
||||
finalSfId = _channels[ch].sfId;
|
||||
var cachedCh = _channels[ch];
|
||||
// The note's own synth engine (track instrument) is
|
||||
// authoritative. Channel state is only a cache: it must never
|
||||
// mask the track's instrument, otherwise multi-track ARM or a
|
||||
// re-picked instrument plays the wrong soundfont. Without an
|
||||
// engine, fall back to the soundfont configured on the channel.
|
||||
if (!synthEngine && cachedCh && cachedCh.sfId !== undefined) {
|
||||
finalBank = parseInt(cachedCh.bank) || 0;
|
||||
finalProg = parseInt(cachedCh.program) || 0;
|
||||
finalSfId = cachedCh.sfId;
|
||||
}
|
||||
// Ensure the soundfont is actually loaded before the note plays.
|
||||
// Quick instrument pick on a track does not pre-load it, so load
|
||||
// lazily here and retry the note once the font is ready.
|
||||
if (finalSfId && !_sfHandleMap.has(finalSfId)) {
|
||||
// Cooldown lỗi: font 404 → KHÔNG spam fetch mỗi note (10s)
|
||||
// — note chạy thẳng fallback để CÓ ÂM.
|
||||
var _lastFail = _sfLoadFailAt[finalSfId] || 0;
|
||||
if (Date.now() - _lastFail < 10000) {
|
||||
try { self._playNoteFallback(note, velocity, durationMs, startTime, program, null, channel, synthEngine); } catch (e) {}
|
||||
return;
|
||||
}
|
||||
console.log('[SonicSF] soundfont not loaded yet, loading:', finalSfId);
|
||||
self.loadSoundFont(finalSfId).then(function (ok) {
|
||||
console.log('[SonicSF] loadSoundFont result:', ok, 'for:', finalSfId);
|
||||
if (ok) {
|
||||
doNote();
|
||||
} else {
|
||||
// Font KHÔNG tải được (404/format) → KHÔNG drop note
|
||||
// câm lặng ("bỏ qua WASM") — fallback oscillator.
|
||||
_sfLoadFailAt[finalSfId] = Date.now();
|
||||
try { self._playNoteFallback(note, velocity, durationMs, startTime, program, null, channel, synthEngine); } catch (e) {}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Program change at note time, not call time — ensures correct
|
||||
// instrument for each item regardless of processing order.
|
||||
if (synthEngine || program !== undefined) {
|
||||
// Skip if the channel already has this exact instrument (avoids
|
||||
// per-note soundfont reloads that cause audible crackle/glitches).
|
||||
// ⚠️ Chỉ skip khi handle SF vẫn CÒN HỢP LỆ trong map — nếu
|
||||
// không → vẫn program_select lại (tránh dùng handle đã unload).
|
||||
var progAlreadySet = cachedCh && cachedCh.program === finalProg && cachedCh.bank === finalBank && cachedCh.sfId === finalSfId
|
||||
&& (finalSfId ? _sfHandleMap.has(finalSfId) : true);
|
||||
if ((synthEngine || program !== undefined) && !progAlreadySet) {
|
||||
var sfHandle = finalSfId ? _sfHandleMap.get(finalSfId) : undefined;
|
||||
console.log('[SonicSF] selectProgram for channel:', ch, 'sfHandle:', sfHandle, 'bank:', finalBank, 'prog:', finalProg);
|
||||
if (sfHandle !== undefined) {
|
||||
try {
|
||||
_fluidModule._fluid_synth_program_select(_synthPtr, ch, sfHandle, finalBank, finalProg);
|
||||
// Percussion (bank 128): tìm preset HỢP LỆ trong
|
||||
// font — quét bank 128 + bank 0 (0-127) MỘT LẦN,
|
||||
// cache theo sfId. Trước đây chỉ thử 4 preset cố
|
||||
// định → font không có → cache channel = (128,0)
|
||||
// INVALID → note sau skip re-select (progAlreadySet)
|
||||
// → noteon preset rỗng = CÂM ("1 âm đầu rồi câm").
|
||||
if (finalBank === 128) {
|
||||
var _vKey = finalSfId || ('h' + sfHandle);
|
||||
if (_validPercCache[_vKey] === undefined) {
|
||||
var _found = null;
|
||||
for (var _b = 0; _b < 2 && !_found; _b++) {
|
||||
var _bk = _b === 0 ? 128 : 0;
|
||||
for (var _p = 0; _p < 128 && !_found; _p++) {
|
||||
try {
|
||||
if (_fluidModule._fluid_synth_program_select(_synthPtr, 9, sfHandle, _bk, _p) === 0) {
|
||||
_found = [_bk, _p];
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
_validPercCache[_vKey] = _found;
|
||||
}
|
||||
if (_validPercCache[_vKey]) {
|
||||
finalBank = _validPercCache[_vKey][0];
|
||||
finalProg = _validPercCache[_vKey][1];
|
||||
}
|
||||
}
|
||||
var _selRet = _fluidModule._fluid_synth_program_select(_synthPtr, ch, sfHandle, finalBank, finalProg);
|
||||
} catch (e) {}
|
||||
} else {
|
||||
try { _fluidModule._fluid_synth_bank_select(_synthPtr, ch, finalBank); } catch (e) {}
|
||||
try { _fluidModule._fluid_synth_program_change(_synthPtr, ch, finalProg); } catch (e) {}
|
||||
}
|
||||
if (!_channels[ch]) _channels[ch] = {};
|
||||
_channels[ch].bank = finalBank;
|
||||
_channels[ch].program = finalProg;
|
||||
_channels[ch].sfId = finalSfId;
|
||||
}
|
||||
console.log("[SonicSF] noteOn ch:", ch, "pitch:", midiPitch, "vel:", midiVel);
|
||||
console.log('[SonicSF] noteon channel:', ch, 'pitch:', midiPitch, 'vel:', midiVel, 'sfId:', finalSfId);
|
||||
_fluidModule._fluid_synth_noteon(_synthPtr, ch, midiPitch, midiVel);
|
||||
var noteMapKey = (_origChannel !== undefined ? _origChannel : 0) + ':' + midiPitch;
|
||||
if (!_activeNotes[noteMapKey]) _activeNotes[noteMapKey] = [];
|
||||
@@ -446,7 +644,7 @@
|
||||
if (arr.length === 0) delete _activeNotes[noteMapKey];
|
||||
}
|
||||
} catch (e) {}
|
||||
}, durSec * 1000);
|
||||
}, durationMs);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[SonicSF] FluidSynth noteOn error:", e);
|
||||
@@ -461,6 +659,31 @@
|
||||
}
|
||||
},
|
||||
|
||||
// Hủy mọi note-on được schedule (tương lai) + note-off mọi notes đang
|
||||
// ngân — gọi khi STOP/PAUSE để hết "âm thanh bị stuck" (note-on chưa
|
||||
// bắn vẫn bắn sau khi dừng; notes durationMs>=60000 không có note-off
|
||||
// tự động → ngân vô hạn → VU master nhảy dù không play).
|
||||
panic: function () {
|
||||
_scheduledNotes.forEach(function (sn) { if (sn.on) { clearTimeout(sn.on); sn.on = null; } });
|
||||
_scheduledNotes = [];
|
||||
if (_initialized && _fluidModule) {
|
||||
// noteoff TỪNG note đang ngân (binding _fluid_synth_noteoff chắc
|
||||
// chắn tồn tại — đã dùng cho duration hết) — all_notes_off có
|
||||
// thể không có trong WASM exports (catch nuốt → notes kẹt).
|
||||
Object.keys(_activeNotes).forEach(function (key) {
|
||||
var parts = key.split(':');
|
||||
var pitch = parseInt(parts[1], 10);
|
||||
(_activeNotes[key] || []).forEach(function (ch) {
|
||||
try { _fluidModule._fluid_synth_noteoff(_synthPtr, ch, pitch); } catch (e) {}
|
||||
});
|
||||
});
|
||||
try {
|
||||
for (var c = 0; c < 16; c++) _fluidModule._fluid_synth_all_notes_off(_synthPtr, c);
|
||||
} catch (e) {}
|
||||
}
|
||||
_activeNotes = {};
|
||||
},
|
||||
|
||||
_playNoteFallback: function (note, velocity, durationMs, startTime, program, destinationNode, channel, synthEngine) {
|
||||
var ctx = getCtx();
|
||||
var freq = 440 * Math.pow(2, (note - 69) / 12);
|
||||
@@ -477,7 +700,11 @@
|
||||
var oscType = 'triangle';
|
||||
var attackTime = 0.03, decayTime = 0.1, sustainLevel = 0.5, releaseTime = 0.2, volFactor = 0.25;
|
||||
var prog = program !== undefined ? parseInt(program) : 0;
|
||||
if (channel !== undefined && channel >= 0 && channel < 16) {
|
||||
// CHỈ dùng cache channel khi KHÔNG có program/synthEngine được
|
||||
// truyền — trước đây override program của track bằng cache channel
|
||||
// (bị track khác cùng channel ghi đè → preview note vẽ mới mang
|
||||
// nhạc cụ của track TRƯỚC).
|
||||
if (program === undefined && channel !== undefined && channel >= 0 && channel < 16) {
|
||||
prog = _channels[channel].program || prog;
|
||||
}
|
||||
if (prog >= 0 && prog <= 7) { oscType = 'sine'; decayTime = 0.3; sustainLevel = 0.1; releaseTime = 0.2; }
|
||||
@@ -495,7 +722,8 @@
|
||||
osc.frequency.setValueAtTime(freq, 0);
|
||||
var startAt = startTime !== undefined ? startTime : ctx.currentTime;
|
||||
var durSec = durationMs / 1000;
|
||||
var vel = typeof velocity === 'number' ? (velocity > 1 ? velocity / 127 : velocity) : 0.8;
|
||||
var vel = (typeof velocity === 'number' && isFinite(velocity) && !isNaN(velocity)) ? (velocity > 1 ? velocity / 127 : velocity) : 0.8;
|
||||
if (isNaN(vel)) vel = 0.8;
|
||||
var targetGain = vel * volFactor;
|
||||
noteGain.gain.setValueAtTime(0, startAt);
|
||||
noteGain.gain.linearRampToValueAtTime(targetGain, startAt + attackTime);
|
||||
@@ -516,9 +744,17 @@
|
||||
|
||||
stopAll: function () {
|
||||
if (_initialized && _fluidModule) {
|
||||
// noteoff từng note đang ngân (binding chắc chắn tồn tại) —
|
||||
// phòng all_notes_off không có trong WASM exports.
|
||||
Object.keys(_activeNotes).forEach(function (key) {
|
||||
var parts = key.split(':');
|
||||
var pitch = parseInt(parts[1], 10);
|
||||
(_activeNotes[key] || []).forEach(function (ch) {
|
||||
try { _fluidModule._fluid_synth_noteoff(_synthPtr, ch, pitch); } catch (e) {}
|
||||
});
|
||||
});
|
||||
for (var ch = 0; ch < 16; ch++) {
|
||||
try { _fluidModule._fluid_synth_all_notes_off(_synthPtr, ch); } catch (e) {}
|
||||
try { _fluidModule._fluid_synth_all_sounds_off(_synthPtr, ch); } catch (e) {}
|
||||
}
|
||||
}
|
||||
while (_scheduledNotes.length > 0) {
|
||||
@@ -535,6 +771,7 @@
|
||||
} catch (e) {}
|
||||
});
|
||||
Object.keys(_activeOscillators).forEach(function (k) { delete _activeOscillators[k]; });
|
||||
_activeNotes = {};
|
||||
},
|
||||
|
||||
saveToIndexedDB: async function (name, arrayBuffer) {
|
||||
@@ -559,10 +796,13 @@
|
||||
var leftPtr = _leftBufPtr;
|
||||
var rightPtr = _rightBufPtr;
|
||||
var block = RENDER_BLOCK;
|
||||
var queueDepth = 0;
|
||||
var maxQueue = QUEUE_TARGET;
|
||||
var queueDepth = 0;
|
||||
var lastTick = performance.now();
|
||||
var frameMs = (block / _audioCtx.sampleRate) * 1000;
|
||||
|
||||
var _dbgPeak = 0;
|
||||
// Track consumption by wall-clock time instead of async messages — immune
|
||||
// to message-latency races that could underrun (silence gaps → crackle).
|
||||
function pushFrame() {
|
||||
if (!Module || !synth || !node) return;
|
||||
try {
|
||||
@@ -573,18 +813,6 @@
|
||||
Module._fluid_synth_write_float(synth, block, leftPtr, 0, 1, rightPtr, 0, 1);
|
||||
var leftArr = new Float32Array(Module.HEAPF32.subarray(lpb, lpb + block));
|
||||
var rightArr = new Float32Array(Module.HEAPF32.subarray(rpb, rpb + block));
|
||||
var peak = 0;
|
||||
var avg = 0;
|
||||
for (var si = 0; si < leftArr.length; si++) {
|
||||
var abs = leftArr[si] > 0 ? leftArr[si] : -leftArr[si];
|
||||
if (abs > peak) peak = abs;
|
||||
avg += abs;
|
||||
}
|
||||
avg /= leftArr.length;
|
||||
if (!_dbgPeak) {
|
||||
_dbgPeak = 1;
|
||||
console.log("[SonicSF] FRAME peak:", peak.toFixed(6), "avg:", avg.toFixed(8), "gain check:", Module._fluid_synth_get_gain ? Module._fluid_synth_get_gain(synth) : 'N/A');
|
||||
}
|
||||
node.port.postMessage({ type: 'PCM', L: leftArr, R: rightArr }, [leftArr.buffer, rightArr.buffer]);
|
||||
queueDepth++;
|
||||
} catch (e) { console.warn("[SonicSF] pushFrame error:", e); }
|
||||
@@ -595,14 +823,16 @@
|
||||
_renderTimer = null;
|
||||
return;
|
||||
}
|
||||
var needed = maxQueue - queueDepth;
|
||||
var now = performance.now();
|
||||
queueDepth = Math.max(0, queueDepth - (now - lastTick) / frameMs);
|
||||
lastTick = now;
|
||||
var needed = Math.min(maxQueue - queueDepth, maxQueue);
|
||||
for (var i = 0; i < needed; i++) {
|
||||
pushFrame();
|
||||
}
|
||||
queueDepth = Math.max(0, queueDepth - 1);
|
||||
}
|
||||
|
||||
_renderTimer = setInterval(fillLoop, Math.max(8, (block / _audioCtx.sampleRate) * 1000 * 0.75));
|
||||
_renderTimer = setInterval(fillLoop, Math.max(4, frameMs * 0.5));
|
||||
}
|
||||
|
||||
function _stopRenderLoop() {
|
||||
|
||||
@@ -60,11 +60,13 @@
|
||||
}
|
||||
|
||||
let autoSaveTimer = null;
|
||||
let lastGetProjectStateCallback = null;
|
||||
function scheduleTempAutoSave(getProjectStateCallback) {
|
||||
if (getProjectStateCallback) lastGetProjectStateCallback = getProjectStateCallback;
|
||||
if (autoSaveTimer) clearTimeout(autoSaveTimer);
|
||||
autoSaveTimer = setTimeout(async () => {
|
||||
try {
|
||||
const state = getProjectStateCallback();
|
||||
const state = lastGetProjectStateCallback ? lastGetProjectStateCallback() : null;
|
||||
if (!state || (!state.tracks && !state.main_session)) return;
|
||||
const dataJson = JSON.stringify(state);
|
||||
localStorage.setItem('sonic_temp_project', dataJson);
|
||||
@@ -76,10 +78,26 @@
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
// Lưu NGAY (bỏ debounce 2s) — dùng cho thay đổi cần bền vững tức thì (đổi màu track)
|
||||
async function flushTempAutoSave() {
|
||||
if (autoSaveTimer) { clearTimeout(autoSaveTimer); autoSaveTimer = null; }
|
||||
try {
|
||||
const state = lastGetProjectStateCallback ? lastGetProjectStateCallback() : null;
|
||||
if (!state || (!state.tracks && !state.main_session)) return;
|
||||
const dataJson = JSON.stringify(state);
|
||||
localStorage.setItem('sonic_temp_project', dataJson);
|
||||
if (window.SonicAPI && localStorage.getItem('sonic_token')) {
|
||||
await window.SonicAPI.saveTempProject(dataJson).catch(() => {});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Flush temp project warning:", e);
|
||||
}
|
||||
}
|
||||
|
||||
window.SonicStorage = {
|
||||
exportProjectToSFS,
|
||||
importProjectFromSFSFile,
|
||||
scheduleTempAutoSave
|
||||
scheduleTempAutoSave,
|
||||
flushTempAutoSave
|
||||
};
|
||||
})();
|
||||
|
||||
Vendored
+12
File diff suppressed because one or more lines are too long
+29924
File diff suppressed because it is too large
Load Diff
+267
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* @license React
|
||||
* react-dom.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
(function(){/*
|
||||
Modernizr 3.0.0pre (Custom Build) | MIT
|
||||
*/
|
||||
'use strict';(function(Q,mb){"object"===typeof exports&&"undefined"!==typeof module?mb(exports,require("react")):"function"===typeof define&&define.amd?define(["exports","react"],mb):(Q=Q||self,mb(Q.ReactDOM={},Q.React))})(this,function(Q,mb){function n(a){for(var b="https://reactjs.org/docs/error-decoder.html?invariant="+a,c=1;c<arguments.length;c++)b+="&args[]="+encodeURIComponent(arguments[c]);return"Minified React error #"+a+"; visit "+b+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}
|
||||
function nb(a,b){Ab(a,b);Ab(a+"Capture",b)}function Ab(a,b){$b[a]=b;for(a=0;a<b.length;a++)cg.add(b[a])}function cj(a){if(Zd.call(dg,a))return!0;if(Zd.call(eg,a))return!1;if(dj.test(a))return dg[a]=!0;eg[a]=!0;return!1}function ej(a,b,c,d){if(null!==c&&0===c.type)return!1;switch(typeof b){case "function":case "symbol":return!0;case "boolean":if(d)return!1;if(null!==c)return!c.acceptsBooleans;a=a.toLowerCase().slice(0,5);return"data-"!==a&&"aria-"!==a;default:return!1}}function fj(a,b,c,d){if(null===
|
||||
b||"undefined"===typeof b||ej(a,b,c,d))return!0;if(d)return!1;if(null!==c)switch(c.type){case 3:return!b;case 4:return!1===b;case 5:return isNaN(b);case 6:return isNaN(b)||1>b}return!1}function Y(a,b,c,d,e,f,g){this.acceptsBooleans=2===b||3===b||4===b;this.attributeName=d;this.attributeNamespace=e;this.mustUseProperty=c;this.propertyName=a;this.type=b;this.sanitizeURL=f;this.removeEmptyString=g}function $d(a,b,c,d){var e=R.hasOwnProperty(b)?R[b]:null;if(null!==e?0!==e.type:d||!(2<b.length)||"o"!==
|
||||
b[0]&&"O"!==b[0]||"n"!==b[1]&&"N"!==b[1])fj(b,c,e,d)&&(c=null),d||null===e?cj(b)&&(null===c?a.removeAttribute(b):a.setAttribute(b,""+c)):e.mustUseProperty?a[e.propertyName]=null===c?3===e.type?!1:"":c:(b=e.attributeName,d=e.attributeNamespace,null===c?a.removeAttribute(b):(e=e.type,c=3===e||4===e&&!0===c?"":""+c,d?a.setAttributeNS(d,b,c):a.setAttribute(b,c)))}function ac(a){if(null===a||"object"!==typeof a)return null;a=fg&&a[fg]||a["@@iterator"];return"function"===typeof a?a:null}function bc(a,b,
|
||||
c){if(void 0===ae)try{throw Error();}catch(d){ae=(b=d.stack.trim().match(/\n( *(at )?)/))&&b[1]||""}return"\n"+ae+a}function be(a,b){if(!a||ce)return"";ce=!0;var c=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{if(b)if(b=function(){throw Error();},Object.defineProperty(b.prototype,"props",{set:function(){throw Error();}}),"object"===typeof Reflect&&Reflect.construct){try{Reflect.construct(b,[])}catch(m){var d=m}Reflect.construct(a,[],b)}else{try{b.call()}catch(m){d=m}a.call(b.prototype)}else{try{throw Error();
|
||||
}catch(m){d=m}a()}}catch(m){if(m&&d&&"string"===typeof m.stack){for(var e=m.stack.split("\n"),f=d.stack.split("\n"),g=e.length-1,h=f.length-1;1<=g&&0<=h&&e[g]!==f[h];)h--;for(;1<=g&&0<=h;g--,h--)if(e[g]!==f[h]){if(1!==g||1!==h){do if(g--,h--,0>h||e[g]!==f[h]){var k="\n"+e[g].replace(" at new "," at ");a.displayName&&k.includes("<anonymous>")&&(k=k.replace("<anonymous>",a.displayName));return k}while(1<=g&&0<=h)}break}}}finally{ce=!1,Error.prepareStackTrace=c}return(a=a?a.displayName||a.name:"")?bc(a):
|
||||
""}function gj(a){switch(a.tag){case 5:return bc(a.type);case 16:return bc("Lazy");case 13:return bc("Suspense");case 19:return bc("SuspenseList");case 0:case 2:case 15:return a=be(a.type,!1),a;case 11:return a=be(a.type.render,!1),a;case 1:return a=be(a.type,!0),a;default:return""}}function de(a){if(null==a)return null;if("function"===typeof a)return a.displayName||a.name||null;if("string"===typeof a)return a;switch(a){case Bb:return"Fragment";case Cb:return"Portal";case ee:return"Profiler";case fe:return"StrictMode";
|
||||
case ge:return"Suspense";case he:return"SuspenseList"}if("object"===typeof a)switch(a.$$typeof){case gg:return(a.displayName||"Context")+".Consumer";case hg:return(a._context.displayName||"Context")+".Provider";case ie:var b=a.render;a=a.displayName;a||(a=b.displayName||b.name||"",a=""!==a?"ForwardRef("+a+")":"ForwardRef");return a;case je:return b=a.displayName||null,null!==b?b:de(a.type)||"Memo";case Ta:b=a._payload;a=a._init;try{return de(a(b))}catch(c){}}return null}function hj(a){var b=a.type;
|
||||
switch(a.tag){case 24:return"Cache";case 9:return(b.displayName||"Context")+".Consumer";case 10:return(b._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return a=b.render,a=a.displayName||a.name||"",b.displayName||(""!==a?"ForwardRef("+a+")":"ForwardRef");case 7:return"Fragment";case 5:return b;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return de(b);case 8:return b===fe?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";
|
||||
case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if("function"===typeof b)return b.displayName||b.name||null;if("string"===typeof b)return b}return null}function Ua(a){switch(typeof a){case "boolean":case "number":case "string":case "undefined":return a;case "object":return a;default:return""}}function ig(a){var b=a.type;return(a=a.nodeName)&&"input"===a.toLowerCase()&&("checkbox"===b||"radio"===
|
||||
b)}function ij(a){var b=ig(a)?"checked":"value",c=Object.getOwnPropertyDescriptor(a.constructor.prototype,b),d=""+a[b];if(!a.hasOwnProperty(b)&&"undefined"!==typeof c&&"function"===typeof c.get&&"function"===typeof c.set){var e=c.get,f=c.set;Object.defineProperty(a,b,{configurable:!0,get:function(){return e.call(this)},set:function(a){d=""+a;f.call(this,a)}});Object.defineProperty(a,b,{enumerable:c.enumerable});return{getValue:function(){return d},setValue:function(a){d=""+a},stopTracking:function(){a._valueTracker=
|
||||
null;delete a[b]}}}}function Pc(a){a._valueTracker||(a._valueTracker=ij(a))}function jg(a){if(!a)return!1;var b=a._valueTracker;if(!b)return!0;var c=b.getValue();var d="";a&&(d=ig(a)?a.checked?"true":"false":a.value);a=d;return a!==c?(b.setValue(a),!0):!1}function Qc(a){a=a||("undefined"!==typeof document?document:void 0);if("undefined"===typeof a)return null;try{return a.activeElement||a.body}catch(b){return a.body}}function ke(a,b){var c=b.checked;return E({},b,{defaultChecked:void 0,defaultValue:void 0,
|
||||
value:void 0,checked:null!=c?c:a._wrapperState.initialChecked})}function kg(a,b){var c=null==b.defaultValue?"":b.defaultValue,d=null!=b.checked?b.checked:b.defaultChecked;c=Ua(null!=b.value?b.value:c);a._wrapperState={initialChecked:d,initialValue:c,controlled:"checkbox"===b.type||"radio"===b.type?null!=b.checked:null!=b.value}}function lg(a,b){b=b.checked;null!=b&&$d(a,"checked",b,!1)}function le(a,b){lg(a,b);var c=Ua(b.value),d=b.type;if(null!=c)if("number"===d){if(0===c&&""===a.value||a.value!=
|
||||
c)a.value=""+c}else a.value!==""+c&&(a.value=""+c);else if("submit"===d||"reset"===d){a.removeAttribute("value");return}b.hasOwnProperty("value")?me(a,b.type,c):b.hasOwnProperty("defaultValue")&&me(a,b.type,Ua(b.defaultValue));null==b.checked&&null!=b.defaultChecked&&(a.defaultChecked=!!b.defaultChecked)}function mg(a,b,c){if(b.hasOwnProperty("value")||b.hasOwnProperty("defaultValue")){var d=b.type;if(!("submit"!==d&&"reset"!==d||void 0!==b.value&&null!==b.value))return;b=""+a._wrapperState.initialValue;
|
||||
c||b===a.value||(a.value=b);a.defaultValue=b}c=a.name;""!==c&&(a.name="");a.defaultChecked=!!a._wrapperState.initialChecked;""!==c&&(a.name=c)}function me(a,b,c){if("number"!==b||Qc(a.ownerDocument)!==a)null==c?a.defaultValue=""+a._wrapperState.initialValue:a.defaultValue!==""+c&&(a.defaultValue=""+c)}function Db(a,b,c,d){a=a.options;if(b){b={};for(var e=0;e<c.length;e++)b["$"+c[e]]=!0;for(c=0;c<a.length;c++)e=b.hasOwnProperty("$"+a[c].value),a[c].selected!==e&&(a[c].selected=e),e&&d&&(a[c].defaultSelected=
|
||||
!0)}else{c=""+Ua(c);b=null;for(e=0;e<a.length;e++){if(a[e].value===c){a[e].selected=!0;d&&(a[e].defaultSelected=!0);return}null!==b||a[e].disabled||(b=a[e])}null!==b&&(b.selected=!0)}}function ne(a,b){if(null!=b.dangerouslySetInnerHTML)throw Error(n(91));return E({},b,{value:void 0,defaultValue:void 0,children:""+a._wrapperState.initialValue})}function ng(a,b){var c=b.value;if(null==c){c=b.children;b=b.defaultValue;if(null!=c){if(null!=b)throw Error(n(92));if(cc(c)){if(1<c.length)throw Error(n(93));
|
||||
c=c[0]}b=c}null==b&&(b="");c=b}a._wrapperState={initialValue:Ua(c)}}function og(a,b){var c=Ua(b.value),d=Ua(b.defaultValue);null!=c&&(c=""+c,c!==a.value&&(a.value=c),null==b.defaultValue&&a.defaultValue!==c&&(a.defaultValue=c));null!=d&&(a.defaultValue=""+d)}function pg(a,b){b=a.textContent;b===a._wrapperState.initialValue&&""!==b&&null!==b&&(a.value=b)}function qg(a){switch(a){case "svg":return"http://www.w3.org/2000/svg";case "math":return"http://www.w3.org/1998/Math/MathML";default:return"http://www.w3.org/1999/xhtml"}}
|
||||
function oe(a,b){return null==a||"http://www.w3.org/1999/xhtml"===a?qg(b):"http://www.w3.org/2000/svg"===a&&"foreignObject"===b?"http://www.w3.org/1999/xhtml":a}function rg(a,b,c){return null==b||"boolean"===typeof b||""===b?"":c||"number"!==typeof b||0===b||dc.hasOwnProperty(a)&&dc[a]?(""+b).trim():b+"px"}function sg(a,b){a=a.style;for(var c in b)if(b.hasOwnProperty(c)){var d=0===c.indexOf("--"),e=rg(c,b[c],d);"float"===c&&(c="cssFloat");d?a.setProperty(c,e):a[c]=e}}function pe(a,b){if(b){if(jj[a]&&
|
||||
(null!=b.children||null!=b.dangerouslySetInnerHTML))throw Error(n(137,a));if(null!=b.dangerouslySetInnerHTML){if(null!=b.children)throw Error(n(60));if("object"!==typeof b.dangerouslySetInnerHTML||!("__html"in b.dangerouslySetInnerHTML))throw Error(n(61));}if(null!=b.style&&"object"!==typeof b.style)throw Error(n(62));}}function qe(a,b){if(-1===a.indexOf("-"))return"string"===typeof b.is;switch(a){case "annotation-xml":case "color-profile":case "font-face":case "font-face-src":case "font-face-uri":case "font-face-format":case "font-face-name":case "missing-glyph":return!1;
|
||||
default:return!0}}function re(a){a=a.target||a.srcElement||window;a.correspondingUseElement&&(a=a.correspondingUseElement);return 3===a.nodeType?a.parentNode:a}function tg(a){if(a=ec(a)){if("function"!==typeof se)throw Error(n(280));var b=a.stateNode;b&&(b=Rc(b),se(a.stateNode,a.type,b))}}function ug(a){Eb?Fb?Fb.push(a):Fb=[a]:Eb=a}function vg(){if(Eb){var a=Eb,b=Fb;Fb=Eb=null;tg(a);if(b)for(a=0;a<b.length;a++)tg(b[a])}}function wg(a,b,c){if(te)return a(b,c);te=!0;try{return xg(a,b,c)}finally{if(te=
|
||||
!1,null!==Eb||null!==Fb)yg(),vg()}}function fc(a,b){var c=a.stateNode;if(null===c)return null;var d=Rc(c);if(null===d)return null;c=d[b];a:switch(b){case "onClick":case "onClickCapture":case "onDoubleClick":case "onDoubleClickCapture":case "onMouseDown":case "onMouseDownCapture":case "onMouseMove":case "onMouseMoveCapture":case "onMouseUp":case "onMouseUpCapture":case "onMouseEnter":(d=!d.disabled)||(a=a.type,d=!("button"===a||"input"===a||"select"===a||"textarea"===a));a=!d;break a;default:a=!1}if(a)return null;
|
||||
if(c&&"function"!==typeof c)throw Error(n(231,b,typeof c));return c}function kj(a,b,c,d,e,f,g,h,k){gc=!1;Sc=null;lj.apply(mj,arguments)}function nj(a,b,c,d,e,f,g,h,k){kj.apply(this,arguments);if(gc){if(gc){var m=Sc;gc=!1;Sc=null}else throw Error(n(198));Tc||(Tc=!0,ue=m)}}function ob(a){var b=a,c=a;if(a.alternate)for(;b.return;)b=b.return;else{a=b;do b=a,0!==(b.flags&4098)&&(c=b.return),a=b.return;while(a)}return 3===b.tag?c:null}function zg(a){if(13===a.tag){var b=a.memoizedState;null===b&&(a=a.alternate,
|
||||
null!==a&&(b=a.memoizedState));if(null!==b)return b.dehydrated}return null}function Ag(a){if(ob(a)!==a)throw Error(n(188));}function oj(a){var b=a.alternate;if(!b){b=ob(a);if(null===b)throw Error(n(188));return b!==a?null:a}for(var c=a,d=b;;){var e=c.return;if(null===e)break;var f=e.alternate;if(null===f){d=e.return;if(null!==d){c=d;continue}break}if(e.child===f.child){for(f=e.child;f;){if(f===c)return Ag(e),a;if(f===d)return Ag(e),b;f=f.sibling}throw Error(n(188));}if(c.return!==d.return)c=e,d=f;
|
||||
else{for(var g=!1,h=e.child;h;){if(h===c){g=!0;c=e;d=f;break}if(h===d){g=!0;d=e;c=f;break}h=h.sibling}if(!g){for(h=f.child;h;){if(h===c){g=!0;c=f;d=e;break}if(h===d){g=!0;d=f;c=e;break}h=h.sibling}if(!g)throw Error(n(189));}}if(c.alternate!==d)throw Error(n(190));}if(3!==c.tag)throw Error(n(188));return c.stateNode.current===c?a:b}function Bg(a){a=oj(a);return null!==a?Cg(a):null}function Cg(a){if(5===a.tag||6===a.tag)return a;for(a=a.child;null!==a;){var b=Cg(a);if(null!==b)return b;a=a.sibling}return null}
|
||||
function pj(a,b){if(Ca&&"function"===typeof Ca.onCommitFiberRoot)try{Ca.onCommitFiberRoot(Uc,a,void 0,128===(a.current.flags&128))}catch(c){}}function qj(a){a>>>=0;return 0===a?32:31-(rj(a)/sj|0)|0}function hc(a){switch(a&-a){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return a&
|
||||
4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return a&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return a}}function Vc(a,b){var c=a.pendingLanes;if(0===c)return 0;var d=0,e=a.suspendedLanes,f=a.pingedLanes,g=c&268435455;if(0!==g){var h=g&~e;0!==h?d=hc(h):(f&=g,0!==f&&(d=hc(f)))}else g=c&~e,0!==g?d=hc(g):0!==f&&(d=hc(f));if(0===d)return 0;if(0!==b&&b!==d&&0===(b&e)&&
|
||||
(e=d&-d,f=b&-b,e>=f||16===e&&0!==(f&4194240)))return b;0!==(d&4)&&(d|=c&16);b=a.entangledLanes;if(0!==b)for(a=a.entanglements,b&=d;0<b;)c=31-ta(b),e=1<<c,d|=a[c],b&=~e;return d}function tj(a,b){switch(a){case 1:case 2:case 4:return b+250;case 8:case 16:case 32:case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return b+5E3;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return-1;
|
||||
case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function uj(a,b){for(var c=a.suspendedLanes,d=a.pingedLanes,e=a.expirationTimes,f=a.pendingLanes;0<f;){var g=31-ta(f),h=1<<g,k=e[g];if(-1===k){if(0===(h&c)||0!==(h&d))e[g]=tj(h,b)}else k<=b&&(a.expiredLanes|=h);f&=~h}}function ve(a){a=a.pendingLanes&-1073741825;return 0!==a?a:a&1073741824?1073741824:0}function Dg(){var a=Wc;Wc<<=1;0===(Wc&4194240)&&(Wc=64);return a}function we(a){for(var b=[],c=0;31>c;c++)b.push(a);
|
||||
return b}function ic(a,b,c){a.pendingLanes|=b;536870912!==b&&(a.suspendedLanes=0,a.pingedLanes=0);a=a.eventTimes;b=31-ta(b);a[b]=c}function vj(a,b){var c=a.pendingLanes&~b;a.pendingLanes=b;a.suspendedLanes=0;a.pingedLanes=0;a.expiredLanes&=b;a.mutableReadLanes&=b;a.entangledLanes&=b;b=a.entanglements;var d=a.eventTimes;for(a=a.expirationTimes;0<c;){var e=31-ta(c),f=1<<e;b[e]=0;d[e]=-1;a[e]=-1;c&=~f}}function xe(a,b){var c=a.entangledLanes|=b;for(a=a.entanglements;c;){var d=31-ta(c),e=1<<d;e&b|a[d]&
|
||||
b&&(a[d]|=b);c&=~e}}function Eg(a){a&=-a;return 1<a?4<a?0!==(a&268435455)?16:536870912:4:1}function Fg(a,b){switch(a){case "focusin":case "focusout":Va=null;break;case "dragenter":case "dragleave":Wa=null;break;case "mouseover":case "mouseout":Xa=null;break;case "pointerover":case "pointerout":jc.delete(b.pointerId);break;case "gotpointercapture":case "lostpointercapture":kc.delete(b.pointerId)}}function lc(a,b,c,d,e,f){if(null===a||a.nativeEvent!==f)return a={blockedOn:b,domEventName:c,eventSystemFlags:d,
|
||||
nativeEvent:f,targetContainers:[e]},null!==b&&(b=ec(b),null!==b&&Gg(b)),a;a.eventSystemFlags|=d;b=a.targetContainers;null!==e&&-1===b.indexOf(e)&&b.push(e);return a}function wj(a,b,c,d,e){switch(b){case "focusin":return Va=lc(Va,a,b,c,d,e),!0;case "dragenter":return Wa=lc(Wa,a,b,c,d,e),!0;case "mouseover":return Xa=lc(Xa,a,b,c,d,e),!0;case "pointerover":var f=e.pointerId;jc.set(f,lc(jc.get(f)||null,a,b,c,d,e));return!0;case "gotpointercapture":return f=e.pointerId,kc.set(f,lc(kc.get(f)||null,a,b,
|
||||
c,d,e)),!0}return!1}function Hg(a){var b=pb(a.target);if(null!==b){var c=ob(b);if(null!==c)if(b=c.tag,13===b){if(b=zg(c),null!==b){a.blockedOn=b;xj(a.priority,function(){yj(c)});return}}else if(3===b&&c.stateNode.current.memoizedState.isDehydrated){a.blockedOn=3===c.tag?c.stateNode.containerInfo:null;return}}a.blockedOn=null}function Xc(a){if(null!==a.blockedOn)return!1;for(var b=a.targetContainers;0<b.length;){var c=ye(a.domEventName,a.eventSystemFlags,b[0],a.nativeEvent);if(null===c){c=a.nativeEvent;
|
||||
var d=new c.constructor(c.type,c);ze=d;c.target.dispatchEvent(d);ze=null}else return b=ec(c),null!==b&&Gg(b),a.blockedOn=c,!1;b.shift()}return!0}function Ig(a,b,c){Xc(a)&&c.delete(b)}function zj(){Ae=!1;null!==Va&&Xc(Va)&&(Va=null);null!==Wa&&Xc(Wa)&&(Wa=null);null!==Xa&&Xc(Xa)&&(Xa=null);jc.forEach(Ig);kc.forEach(Ig)}function mc(a,b){a.blockedOn===b&&(a.blockedOn=null,Ae||(Ae=!0,Jg(Kg,zj)))}function nc(a){if(0<Yc.length){mc(Yc[0],a);for(var b=1;b<Yc.length;b++){var c=Yc[b];c.blockedOn===a&&(c.blockedOn=
|
||||
null)}}null!==Va&&mc(Va,a);null!==Wa&&mc(Wa,a);null!==Xa&&mc(Xa,a);b=function(b){return mc(b,a)};jc.forEach(b);kc.forEach(b);for(b=0;b<Ya.length;b++)c=Ya[b],c.blockedOn===a&&(c.blockedOn=null);for(;0<Ya.length&&(b=Ya[0],null===b.blockedOn);)Hg(b),null===b.blockedOn&&Ya.shift()}function Aj(a,b,c,d){var e=z,f=Gb.transition;Gb.transition=null;try{z=1,Be(a,b,c,d)}finally{z=e,Gb.transition=f}}function Bj(a,b,c,d){var e=z,f=Gb.transition;Gb.transition=null;try{z=4,Be(a,b,c,d)}finally{z=e,Gb.transition=
|
||||
f}}function Be(a,b,c,d){if(Zc){var e=ye(a,b,c,d);if(null===e)Ce(a,b,d,$c,c),Fg(a,d);else if(wj(e,a,b,c,d))d.stopPropagation();else if(Fg(a,d),b&4&&-1<Cj.indexOf(a)){for(;null!==e;){var f=ec(e);null!==f&&Dj(f);f=ye(a,b,c,d);null===f&&Ce(a,b,d,$c,c);if(f===e)break;e=f}null!==e&&d.stopPropagation()}else Ce(a,b,d,null,c)}}function ye(a,b,c,d){$c=null;a=re(d);a=pb(a);if(null!==a)if(b=ob(a),null===b)a=null;else if(c=b.tag,13===c){a=zg(b);if(null!==a)return a;a=null}else if(3===c){if(b.stateNode.current.memoizedState.isDehydrated)return 3===
|
||||
b.tag?b.stateNode.containerInfo:null;a=null}else b!==a&&(a=null);$c=a;return null}function Lg(a){switch(a){case "cancel":case "click":case "close":case "contextmenu":case "copy":case "cut":case "auxclick":case "dblclick":case "dragend":case "dragstart":case "drop":case "focusin":case "focusout":case "input":case "invalid":case "keydown":case "keypress":case "keyup":case "mousedown":case "mouseup":case "paste":case "pause":case "play":case "pointercancel":case "pointerdown":case "pointerup":case "ratechange":case "reset":case "resize":case "seeked":case "submit":case "touchcancel":case "touchend":case "touchstart":case "volumechange":case "change":case "selectionchange":case "textInput":case "compositionstart":case "compositionend":case "compositionupdate":case "beforeblur":case "afterblur":case "beforeinput":case "blur":case "fullscreenchange":case "focus":case "hashchange":case "popstate":case "select":case "selectstart":return 1;
|
||||
case "drag":case "dragenter":case "dragexit":case "dragleave":case "dragover":case "mousemove":case "mouseout":case "mouseover":case "pointermove":case "pointerout":case "pointerover":case "scroll":case "toggle":case "touchmove":case "wheel":case "mouseenter":case "mouseleave":case "pointerenter":case "pointerleave":return 4;case "message":switch(Ej()){case De:return 1;case Mg:return 4;case ad:case Fj:return 16;case Ng:return 536870912;default:return 16}default:return 16}}function Og(){if(bd)return bd;
|
||||
var a,b=Ee,c=b.length,d,e="value"in Za?Za.value:Za.textContent,f=e.length;for(a=0;a<c&&b[a]===e[a];a++);var g=c-a;for(d=1;d<=g&&b[c-d]===e[f-d];d++);return bd=e.slice(a,1<d?1-d:void 0)}function cd(a){var b=a.keyCode;"charCode"in a?(a=a.charCode,0===a&&13===b&&(a=13)):a=b;10===a&&(a=13);return 32<=a||13===a?a:0}function dd(){return!0}function Pg(){return!1}function ka(a){function b(b,d,e,f,g){this._reactName=b;this._targetInst=e;this.type=d;this.nativeEvent=f;this.target=g;this.currentTarget=null;
|
||||
for(var c in a)a.hasOwnProperty(c)&&(b=a[c],this[c]=b?b(f):f[c]);this.isDefaultPrevented=(null!=f.defaultPrevented?f.defaultPrevented:!1===f.returnValue)?dd:Pg;this.isPropagationStopped=Pg;return this}E(b.prototype,{preventDefault:function(){this.defaultPrevented=!0;var a=this.nativeEvent;a&&(a.preventDefault?a.preventDefault():"unknown"!==typeof a.returnValue&&(a.returnValue=!1),this.isDefaultPrevented=dd)},stopPropagation:function(){var a=this.nativeEvent;a&&(a.stopPropagation?a.stopPropagation():
|
||||
"unknown"!==typeof a.cancelBubble&&(a.cancelBubble=!0),this.isPropagationStopped=dd)},persist:function(){},isPersistent:dd});return b}function Gj(a){var b=this.nativeEvent;return b.getModifierState?b.getModifierState(a):(a=Hj[a])?!!b[a]:!1}function Fe(a){return Gj}function Qg(a,b){switch(a){case "keyup":return-1!==Ij.indexOf(b.keyCode);case "keydown":return 229!==b.keyCode;case "keypress":case "mousedown":case "focusout":return!0;default:return!1}}function Rg(a){a=a.detail;return"object"===typeof a&&
|
||||
"data"in a?a.data:null}function Jj(a,b){switch(a){case "compositionend":return Rg(b);case "keypress":if(32!==b.which)return null;Sg=!0;return Tg;case "textInput":return a=b.data,a===Tg&&Sg?null:a;default:return null}}function Kj(a,b){if(Hb)return"compositionend"===a||!Ge&&Qg(a,b)?(a=Og(),bd=Ee=Za=null,Hb=!1,a):null;switch(a){case "paste":return null;case "keypress":if(!(b.ctrlKey||b.altKey||b.metaKey)||b.ctrlKey&&b.altKey){if(b.char&&1<b.char.length)return b.char;if(b.which)return String.fromCharCode(b.which)}return null;
|
||||
case "compositionend":return Ug&&"ko"!==b.locale?null:b.data;default:return null}}function Vg(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return"input"===b?!!Lj[a.type]:"textarea"===b?!0:!1}function Mj(a){if(!Ia)return!1;a="on"+a;var b=a in document;b||(b=document.createElement("div"),b.setAttribute(a,"return;"),b="function"===typeof b[a]);return b}function Wg(a,b,c,d){ug(d);b=ed(b,"onChange");0<b.length&&(c=new He("onChange","change",null,c,d),a.push({event:c,listeners:b}))}function Nj(a){Xg(a,
|
||||
0)}function fd(a){var b=Ib(a);if(jg(b))return a}function Oj(a,b){if("change"===a)return b}function Yg(){oc&&(oc.detachEvent("onpropertychange",Zg),pc=oc=null)}function Zg(a){if("value"===a.propertyName&&fd(pc)){var b=[];Wg(b,pc,a,re(a));wg(Nj,b)}}function Pj(a,b,c){"focusin"===a?(Yg(),oc=b,pc=c,oc.attachEvent("onpropertychange",Zg)):"focusout"===a&&Yg()}function Qj(a,b){if("selectionchange"===a||"keyup"===a||"keydown"===a)return fd(pc)}function Rj(a,b){if("click"===a)return fd(b)}function Sj(a,b){if("input"===
|
||||
a||"change"===a)return fd(b)}function Tj(a,b){return a===b&&(0!==a||1/a===1/b)||a!==a&&b!==b}function qc(a,b){if(ua(a,b))return!0;if("object"!==typeof a||null===a||"object"!==typeof b||null===b)return!1;var c=Object.keys(a),d=Object.keys(b);if(c.length!==d.length)return!1;for(d=0;d<c.length;d++){var e=c[d];if(!Zd.call(b,e)||!ua(a[e],b[e]))return!1}return!0}function $g(a){for(;a&&a.firstChild;)a=a.firstChild;return a}function ah(a,b){var c=$g(a);a=0;for(var d;c;){if(3===c.nodeType){d=a+c.textContent.length;
|
||||
if(a<=b&&d>=b)return{node:c,offset:b-a};a=d}a:{for(;c;){if(c.nextSibling){c=c.nextSibling;break a}c=c.parentNode}c=void 0}c=$g(c)}}function bh(a,b){return a&&b?a===b?!0:a&&3===a.nodeType?!1:b&&3===b.nodeType?bh(a,b.parentNode):"contains"in a?a.contains(b):a.compareDocumentPosition?!!(a.compareDocumentPosition(b)&16):!1:!1}function ch(){for(var a=window,b=Qc();b instanceof a.HTMLIFrameElement;){try{var c="string"===typeof b.contentWindow.location.href}catch(d){c=!1}if(c)a=b.contentWindow;else break;
|
||||
b=Qc(a.document)}return b}function Ie(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return b&&("input"===b&&("text"===a.type||"search"===a.type||"tel"===a.type||"url"===a.type||"password"===a.type)||"textarea"===b||"true"===a.contentEditable)}function Uj(a){var b=ch(),c=a.focusedElem,d=a.selectionRange;if(b!==c&&c&&c.ownerDocument&&bh(c.ownerDocument.documentElement,c)){if(null!==d&&Ie(c))if(b=d.start,a=d.end,void 0===a&&(a=b),"selectionStart"in c)c.selectionStart=b,c.selectionEnd=Math.min(a,c.value.length);
|
||||
else if(a=(b=c.ownerDocument||document)&&b.defaultView||window,a.getSelection){a=a.getSelection();var e=c.textContent.length,f=Math.min(d.start,e);d=void 0===d.end?f:Math.min(d.end,e);!a.extend&&f>d&&(e=d,d=f,f=e);e=ah(c,f);var g=ah(c,d);e&&g&&(1!==a.rangeCount||a.anchorNode!==e.node||a.anchorOffset!==e.offset||a.focusNode!==g.node||a.focusOffset!==g.offset)&&(b=b.createRange(),b.setStart(e.node,e.offset),a.removeAllRanges(),f>d?(a.addRange(b),a.extend(g.node,g.offset)):(b.setEnd(g.node,g.offset),
|
||||
a.addRange(b)))}b=[];for(a=c;a=a.parentNode;)1===a.nodeType&&b.push({element:a,left:a.scrollLeft,top:a.scrollTop});"function"===typeof c.focus&&c.focus();for(c=0;c<b.length;c++)a=b[c],a.element.scrollLeft=a.left,a.element.scrollTop=a.top}}function dh(a,b,c){var d=c.window===c?c.document:9===c.nodeType?c:c.ownerDocument;Je||null==Jb||Jb!==Qc(d)||(d=Jb,"selectionStart"in d&&Ie(d)?d={start:d.selectionStart,end:d.selectionEnd}:(d=(d.ownerDocument&&d.ownerDocument.defaultView||window).getSelection(),d=
|
||||
{anchorNode:d.anchorNode,anchorOffset:d.anchorOffset,focusNode:d.focusNode,focusOffset:d.focusOffset}),rc&&qc(rc,d)||(rc=d,d=ed(Ke,"onSelect"),0<d.length&&(b=new He("onSelect","select",null,b,c),a.push({event:b,listeners:d}),b.target=Jb)))}function gd(a,b){var c={};c[a.toLowerCase()]=b.toLowerCase();c["Webkit"+a]="webkit"+b;c["Moz"+a]="moz"+b;return c}function hd(a){if(Le[a])return Le[a];if(!Kb[a])return a;var b=Kb[a],c;for(c in b)if(b.hasOwnProperty(c)&&c in eh)return Le[a]=b[c];return a}function $a(a,
|
||||
b){fh.set(a,b);nb(b,[a])}function gh(a,b,c){var d=a.type||"unknown-event";a.currentTarget=c;nj(d,b,void 0,a);a.currentTarget=null}function Xg(a,b){b=0!==(b&4);for(var c=0;c<a.length;c++){var d=a[c],e=d.event;d=d.listeners;a:{var f=void 0;if(b)for(var g=d.length-1;0<=g;g--){var h=d[g],k=h.instance,m=h.currentTarget;h=h.listener;if(k!==f&&e.isPropagationStopped())break a;gh(e,h,m);f=k}else for(g=0;g<d.length;g++){h=d[g];k=h.instance;m=h.currentTarget;h=h.listener;if(k!==f&&e.isPropagationStopped())break a;
|
||||
gh(e,h,m);f=k}}}if(Tc)throw a=ue,Tc=!1,ue=null,a;}function B(a,b){var c=b[Me];void 0===c&&(c=b[Me]=new Set);var d=a+"__bubble";c.has(d)||(hh(b,a,2,!1),c.add(d))}function Ne(a,b,c){var d=0;b&&(d|=4);hh(c,a,d,b)}function sc(a){if(!a[id]){a[id]=!0;cg.forEach(function(b){"selectionchange"!==b&&(Vj.has(b)||Ne(b,!1,a),Ne(b,!0,a))});var b=9===a.nodeType?a:a.ownerDocument;null===b||b[id]||(b[id]=!0,Ne("selectionchange",!1,b))}}function hh(a,b,c,d,e){switch(Lg(b)){case 1:e=Aj;break;case 4:e=Bj;break;default:e=
|
||||
Be}c=e.bind(null,b,c,a);e=void 0;!Oe||"touchstart"!==b&&"touchmove"!==b&&"wheel"!==b||(e=!0);d?void 0!==e?a.addEventListener(b,c,{capture:!0,passive:e}):a.addEventListener(b,c,!0):void 0!==e?a.addEventListener(b,c,{passive:e}):a.addEventListener(b,c,!1)}function Ce(a,b,c,d,e){var f=d;if(0===(b&1)&&0===(b&2)&&null!==d)a:for(;;){if(null===d)return;var g=d.tag;if(3===g||4===g){var h=d.stateNode.containerInfo;if(h===e||8===h.nodeType&&h.parentNode===e)break;if(4===g)for(g=d.return;null!==g;){var k=g.tag;
|
||||
if(3===k||4===k)if(k=g.stateNode.containerInfo,k===e||8===k.nodeType&&k.parentNode===e)return;g=g.return}for(;null!==h;){g=pb(h);if(null===g)return;k=g.tag;if(5===k||6===k){d=f=g;continue a}h=h.parentNode}}d=d.return}wg(function(){var d=f,e=re(c),g=[];a:{var h=fh.get(a);if(void 0!==h){var k=He,n=a;switch(a){case "keypress":if(0===cd(c))break a;case "keydown":case "keyup":k=Wj;break;case "focusin":n="focus";k=Pe;break;case "focusout":n="blur";k=Pe;break;case "beforeblur":case "afterblur":k=Pe;break;
|
||||
case "click":if(2===c.button)break a;case "auxclick":case "dblclick":case "mousedown":case "mousemove":case "mouseup":case "mouseout":case "mouseover":case "contextmenu":k=ih;break;case "drag":case "dragend":case "dragenter":case "dragexit":case "dragleave":case "dragover":case "dragstart":case "drop":k=Xj;break;case "touchcancel":case "touchend":case "touchmove":case "touchstart":k=Yj;break;case jh:case kh:case lh:k=Zj;break;case mh:k=ak;break;case "scroll":k=bk;break;case "wheel":k=ck;break;case "copy":case "cut":case "paste":k=
|
||||
dk;break;case "gotpointercapture":case "lostpointercapture":case "pointercancel":case "pointerdown":case "pointermove":case "pointerout":case "pointerover":case "pointerup":k=nh}var l=0!==(b&4),p=!l&&"scroll"===a,A=l?null!==h?h+"Capture":null:h;l=[];for(var v=d,q;null!==v;){q=v;var M=q.stateNode;5===q.tag&&null!==M&&(q=M,null!==A&&(M=fc(v,A),null!=M&&l.push(tc(v,M,q))));if(p)break;v=v.return}0<l.length&&(h=new k(h,n,null,c,e),g.push({event:h,listeners:l}))}}if(0===(b&7)){a:{h="mouseover"===a||"pointerover"===
|
||||
a;k="mouseout"===a||"pointerout"===a;if(h&&c!==ze&&(n=c.relatedTarget||c.fromElement)&&(pb(n)||n[Ja]))break a;if(k||h){h=e.window===e?e:(h=e.ownerDocument)?h.defaultView||h.parentWindow:window;if(k){if(n=c.relatedTarget||c.toElement,k=d,n=n?pb(n):null,null!==n&&(p=ob(n),n!==p||5!==n.tag&&6!==n.tag))n=null}else k=null,n=d;if(k!==n){l=ih;M="onMouseLeave";A="onMouseEnter";v="mouse";if("pointerout"===a||"pointerover"===a)l=nh,M="onPointerLeave",A="onPointerEnter",v="pointer";p=null==k?h:Ib(k);q=null==
|
||||
n?h:Ib(n);h=new l(M,v+"leave",k,c,e);h.target=p;h.relatedTarget=q;M=null;pb(e)===d&&(l=new l(A,v+"enter",n,c,e),l.target=q,l.relatedTarget=p,M=l);p=M;if(k&&n)b:{l=k;A=n;v=0;for(q=l;q;q=Lb(q))v++;q=0;for(M=A;M;M=Lb(M))q++;for(;0<v-q;)l=Lb(l),v--;for(;0<q-v;)A=Lb(A),q--;for(;v--;){if(l===A||null!==A&&l===A.alternate)break b;l=Lb(l);A=Lb(A)}l=null}else l=null;null!==k&&oh(g,h,k,l,!1);null!==n&&null!==p&&oh(g,p,n,l,!0)}}}a:{h=d?Ib(d):window;k=h.nodeName&&h.nodeName.toLowerCase();if("select"===k||"input"===
|
||||
k&&"file"===h.type)var ma=Oj;else if(Vg(h))if(ph)ma=Sj;else{ma=Qj;var va=Pj}else(k=h.nodeName)&&"input"===k.toLowerCase()&&("checkbox"===h.type||"radio"===h.type)&&(ma=Rj);if(ma&&(ma=ma(a,d))){Wg(g,ma,c,e);break a}va&&va(a,h,d);"focusout"===a&&(va=h._wrapperState)&&va.controlled&&"number"===h.type&&me(h,"number",h.value)}va=d?Ib(d):window;switch(a){case "focusin":if(Vg(va)||"true"===va.contentEditable)Jb=va,Ke=d,rc=null;break;case "focusout":rc=Ke=Jb=null;break;case "mousedown":Je=!0;break;case "contextmenu":case "mouseup":case "dragend":Je=
|
||||
!1;dh(g,c,e);break;case "selectionchange":if(ek)break;case "keydown":case "keyup":dh(g,c,e)}var ab;if(Ge)b:{switch(a){case "compositionstart":var da="onCompositionStart";break b;case "compositionend":da="onCompositionEnd";break b;case "compositionupdate":da="onCompositionUpdate";break b}da=void 0}else Hb?Qg(a,c)&&(da="onCompositionEnd"):"keydown"===a&&229===c.keyCode&&(da="onCompositionStart");da&&(Ug&&"ko"!==c.locale&&(Hb||"onCompositionStart"!==da?"onCompositionEnd"===da&&Hb&&(ab=Og()):(Za=e,Ee=
|
||||
"value"in Za?Za.value:Za.textContent,Hb=!0)),va=ed(d,da),0<va.length&&(da=new qh(da,a,null,c,e),g.push({event:da,listeners:va}),ab?da.data=ab:(ab=Rg(c),null!==ab&&(da.data=ab))));if(ab=fk?Jj(a,c):Kj(a,c))d=ed(d,"onBeforeInput"),0<d.length&&(e=new gk("onBeforeInput","beforeinput",null,c,e),g.push({event:e,listeners:d}),e.data=ab)}Xg(g,b)})}function tc(a,b,c){return{instance:a,listener:b,currentTarget:c}}function ed(a,b){for(var c=b+"Capture",d=[];null!==a;){var e=a,f=e.stateNode;5===e.tag&&null!==
|
||||
f&&(e=f,f=fc(a,c),null!=f&&d.unshift(tc(a,f,e)),f=fc(a,b),null!=f&&d.push(tc(a,f,e)));a=a.return}return d}function Lb(a){if(null===a)return null;do a=a.return;while(a&&5!==a.tag);return a?a:null}function oh(a,b,c,d,e){for(var f=b._reactName,g=[];null!==c&&c!==d;){var h=c,k=h.alternate,m=h.stateNode;if(null!==k&&k===d)break;5===h.tag&&null!==m&&(h=m,e?(k=fc(c,f),null!=k&&g.unshift(tc(c,k,h))):e||(k=fc(c,f),null!=k&&g.push(tc(c,k,h))));c=c.return}0!==g.length&&a.push({event:b,listeners:g})}function rh(a){return("string"===
|
||||
typeof a?a:""+a).replace(hk,"\n").replace(ik,"")}function jd(a,b,c,d){b=rh(b);if(rh(a)!==b&&c)throw Error(n(425));}function kd(){}function Qe(a,b){return"textarea"===a||"noscript"===a||"string"===typeof b.children||"number"===typeof b.children||"object"===typeof b.dangerouslySetInnerHTML&&null!==b.dangerouslySetInnerHTML&&null!=b.dangerouslySetInnerHTML.__html}function jk(a){setTimeout(function(){throw a;})}function Re(a,b){var c=b,d=0;do{var e=c.nextSibling;a.removeChild(c);if(e&&8===e.nodeType)if(c=
|
||||
e.data,"/$"===c){if(0===d){a.removeChild(e);nc(b);return}d--}else"$"!==c&&"$?"!==c&&"$!"!==c||d++;c=e}while(c);nc(b)}function Ka(a){for(;null!=a;a=a.nextSibling){var b=a.nodeType;if(1===b||3===b)break;if(8===b){b=a.data;if("$"===b||"$!"===b||"$?"===b)break;if("/$"===b)return null}}return a}function sh(a){a=a.previousSibling;for(var b=0;a;){if(8===a.nodeType){var c=a.data;if("$"===c||"$!"===c||"$?"===c){if(0===b)return a;b--}else"/$"===c&&b++}a=a.previousSibling}return null}function pb(a){var b=a[Da];
|
||||
if(b)return b;for(var c=a.parentNode;c;){if(b=c[Ja]||c[Da]){c=b.alternate;if(null!==b.child||null!==c&&null!==c.child)for(a=sh(a);null!==a;){if(c=a[Da])return c;a=sh(a)}return b}a=c;c=a.parentNode}return null}function ec(a){a=a[Da]||a[Ja];return!a||5!==a.tag&&6!==a.tag&&13!==a.tag&&3!==a.tag?null:a}function Ib(a){if(5===a.tag||6===a.tag)return a.stateNode;throw Error(n(33));}function Rc(a){return a[uc]||null}function bb(a){return{current:a}}function w(a,b){0>Mb||(a.current=Se[Mb],Se[Mb]=null,Mb--)}
|
||||
function y(a,b,c){Mb++;Se[Mb]=a.current;a.current=b}function Nb(a,b){var c=a.type.contextTypes;if(!c)return cb;var d=a.stateNode;if(d&&d.__reactInternalMemoizedUnmaskedChildContext===b)return d.__reactInternalMemoizedMaskedChildContext;var e={},f;for(f in c)e[f]=b[f];d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=b,a.__reactInternalMemoizedMaskedChildContext=e);return e}function ea(a){a=a.childContextTypes;return null!==a&&void 0!==a}function th(a,b,c){if(J.current!==cb)throw Error(n(168));
|
||||
y(J,b);y(S,c)}function uh(a,b,c){var d=a.stateNode;b=b.childContextTypes;if("function"!==typeof d.getChildContext)return c;d=d.getChildContext();for(var e in d)if(!(e in b))throw Error(n(108,hj(a)||"Unknown",e));return E({},c,d)}function ld(a){a=(a=a.stateNode)&&a.__reactInternalMemoizedMergedChildContext||cb;qb=J.current;y(J,a);y(S,S.current);return!0}function vh(a,b,c){var d=a.stateNode;if(!d)throw Error(n(169));c?(a=uh(a,b,qb),d.__reactInternalMemoizedMergedChildContext=a,w(S),w(J),y(J,a)):w(S);
|
||||
y(S,c)}function wh(a){null===La?La=[a]:La.push(a)}function kk(a){md=!0;wh(a)}function db(){if(!Te&&null!==La){Te=!0;var a=0,b=z;try{var c=La;for(z=1;a<c.length;a++){var d=c[a];do d=d(!0);while(null!==d)}La=null;md=!1}catch(e){throw null!==La&&(La=La.slice(a+1)),xh(De,db),e;}finally{z=b,Te=!1}}return null}function rb(a,b){Ob[Pb++]=nd;Ob[Pb++]=od;od=a;nd=b}function yh(a,b,c){na[oa++]=Ma;na[oa++]=Na;na[oa++]=sb;sb=a;var d=Ma;a=Na;var e=32-ta(d)-1;d&=~(1<<e);c+=1;var f=32-ta(b)+e;if(30<f){var g=e-e%5;
|
||||
f=(d&(1<<g)-1).toString(32);d>>=g;e-=g;Ma=1<<32-ta(b)+e|c<<e|d;Na=f+a}else Ma=1<<f|c<<e|d,Na=a}function Ue(a){null!==a.return&&(rb(a,1),yh(a,1,0))}function Ve(a){for(;a===od;)od=Ob[--Pb],Ob[Pb]=null,nd=Ob[--Pb],Ob[Pb]=null;for(;a===sb;)sb=na[--oa],na[oa]=null,Na=na[--oa],na[oa]=null,Ma=na[--oa],na[oa]=null}function zh(a,b){var c=pa(5,null,null,0);c.elementType="DELETED";c.stateNode=b;c.return=a;b=a.deletions;null===b?(a.deletions=[c],a.flags|=16):b.push(c)}function Ah(a,b){switch(a.tag){case 5:var c=
|
||||
a.type;b=1!==b.nodeType||c.toLowerCase()!==b.nodeName.toLowerCase()?null:b;return null!==b?(a.stateNode=b,la=a,fa=Ka(b.firstChild),!0):!1;case 6:return b=""===a.pendingProps||3!==b.nodeType?null:b,null!==b?(a.stateNode=b,la=a,fa=null,!0):!1;case 13:return b=8!==b.nodeType?null:b,null!==b?(c=null!==sb?{id:Ma,overflow:Na}:null,a.memoizedState={dehydrated:b,treeContext:c,retryLane:1073741824},c=pa(18,null,null,0),c.stateNode=b,c.return=a,a.child=c,la=a,fa=null,!0):!1;default:return!1}}function We(a){return 0!==
|
||||
(a.mode&1)&&0===(a.flags&128)}function Xe(a){if(D){var b=fa;if(b){var c=b;if(!Ah(a,b)){if(We(a))throw Error(n(418));b=Ka(c.nextSibling);var d=la;b&&Ah(a,b)?zh(d,c):(a.flags=a.flags&-4097|2,D=!1,la=a)}}else{if(We(a))throw Error(n(418));a.flags=a.flags&-4097|2;D=!1;la=a}}}function Bh(a){for(a=a.return;null!==a&&5!==a.tag&&3!==a.tag&&13!==a.tag;)a=a.return;la=a}function pd(a){if(a!==la)return!1;if(!D)return Bh(a),D=!0,!1;var b;(b=3!==a.tag)&&!(b=5!==a.tag)&&(b=a.type,b="head"!==b&&"body"!==b&&!Qe(a.type,
|
||||
a.memoizedProps));if(b&&(b=fa)){if(We(a)){for(a=fa;a;)a=Ka(a.nextSibling);throw Error(n(418));}for(;b;)zh(a,b),b=Ka(b.nextSibling)}Bh(a);if(13===a.tag){a=a.memoizedState;a=null!==a?a.dehydrated:null;if(!a)throw Error(n(317));a:{a=a.nextSibling;for(b=0;a;){if(8===a.nodeType){var c=a.data;if("/$"===c){if(0===b){fa=Ka(a.nextSibling);break a}b--}else"$"!==c&&"$!"!==c&&"$?"!==c||b++}a=a.nextSibling}fa=null}}else fa=la?Ka(a.stateNode.nextSibling):null;return!0}function Qb(){fa=la=null;D=!1}function Ye(a){null===
|
||||
wa?wa=[a]:wa.push(a)}function xa(a,b){if(a&&a.defaultProps){b=E({},b);a=a.defaultProps;for(var c in a)void 0===b[c]&&(b[c]=a[c]);return b}return b}function Ze(){$e=Rb=qd=null}function af(a,b){b=rd.current;w(rd);a._currentValue=b}function bf(a,b,c){for(;null!==a;){var d=a.alternate;(a.childLanes&b)!==b?(a.childLanes|=b,null!==d&&(d.childLanes|=b)):null!==d&&(d.childLanes&b)!==b&&(d.childLanes|=b);if(a===c)break;a=a.return}}function Sb(a,b){qd=a;$e=Rb=null;a=a.dependencies;null!==a&&null!==a.firstContext&&
|
||||
(0!==(a.lanes&b)&&(ha=!0),a.firstContext=null)}function qa(a){var b=a._currentValue;if($e!==a)if(a={context:a,memoizedValue:b,next:null},null===Rb){if(null===qd)throw Error(n(308));Rb=a;qd.dependencies={lanes:0,firstContext:a}}else Rb=Rb.next=a;return b}function cf(a){null===tb?tb=[a]:tb.push(a)}function Ch(a,b,c,d){var e=b.interleaved;null===e?(c.next=c,cf(b)):(c.next=e.next,e.next=c);b.interleaved=c;return Oa(a,d)}function Oa(a,b){a.lanes|=b;var c=a.alternate;null!==c&&(c.lanes|=b);c=a;for(a=a.return;null!==
|
||||
a;)a.childLanes|=b,c=a.alternate,null!==c&&(c.childLanes|=b),c=a,a=a.return;return 3===c.tag?c.stateNode:null}function df(a){a.updateQueue={baseState:a.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Dh(a,b){a=a.updateQueue;b.updateQueue===a&&(b.updateQueue={baseState:a.baseState,firstBaseUpdate:a.firstBaseUpdate,lastBaseUpdate:a.lastBaseUpdate,shared:a.shared,effects:a.effects})}function Pa(a,b){return{eventTime:a,lane:b,
|
||||
tag:0,payload:null,callback:null,next:null}}function eb(a,b,c){var d=a.updateQueue;if(null===d)return null;d=d.shared;if(0!==(p&2)){var e=d.pending;null===e?b.next=b:(b.next=e.next,e.next=b);d.pending=b;return lk(a,c)}e=d.interleaved;null===e?(b.next=b,cf(d)):(b.next=e.next,e.next=b);d.interleaved=b;return Oa(a,c)}function sd(a,b,c){b=b.updateQueue;if(null!==b&&(b=b.shared,0!==(c&4194240))){var d=b.lanes;d&=a.pendingLanes;c|=d;b.lanes=c;xe(a,c)}}function Eh(a,b){var c=a.updateQueue,d=a.alternate;
|
||||
if(null!==d&&(d=d.updateQueue,c===d)){var e=null,f=null;c=c.firstBaseUpdate;if(null!==c){do{var g={eventTime:c.eventTime,lane:c.lane,tag:c.tag,payload:c.payload,callback:c.callback,next:null};null===f?e=f=g:f=f.next=g;c=c.next}while(null!==c);null===f?e=f=b:f=f.next=b}else e=f=b;c={baseState:d.baseState,firstBaseUpdate:e,lastBaseUpdate:f,shared:d.shared,effects:d.effects};a.updateQueue=c;return}a=c.lastBaseUpdate;null===a?c.firstBaseUpdate=b:a.next=b;c.lastBaseUpdate=b}function td(a,b,c,d){var e=
|
||||
a.updateQueue;fb=!1;var f=e.firstBaseUpdate,g=e.lastBaseUpdate,h=e.shared.pending;if(null!==h){e.shared.pending=null;var k=h,m=k.next;k.next=null;null===g?f=m:g.next=m;g=k;var n=a.alternate;null!==n&&(n=n.updateQueue,h=n.lastBaseUpdate,h!==g&&(null===h?n.firstBaseUpdate=m:h.next=m,n.lastBaseUpdate=k))}if(null!==f){var l=e.baseState;g=0;n=m=k=null;h=f;do{var r=h.lane,p=h.eventTime;if((d&r)===r){null!==n&&(n=n.next={eventTime:p,lane:0,tag:h.tag,payload:h.payload,callback:h.callback,next:null});a:{var x=
|
||||
a,F=h;r=b;p=c;switch(F.tag){case 1:x=F.payload;if("function"===typeof x){l=x.call(p,l,r);break a}l=x;break a;case 3:x.flags=x.flags&-65537|128;case 0:x=F.payload;r="function"===typeof x?x.call(p,l,r):x;if(null===r||void 0===r)break a;l=E({},l,r);break a;case 2:fb=!0}}null!==h.callback&&0!==h.lane&&(a.flags|=64,r=e.effects,null===r?e.effects=[h]:r.push(h))}else p={eventTime:p,lane:r,tag:h.tag,payload:h.payload,callback:h.callback,next:null},null===n?(m=n=p,k=l):n=n.next=p,g|=r;h=h.next;if(null===h)if(h=
|
||||
e.shared.pending,null===h)break;else r=h,h=r.next,r.next=null,e.lastBaseUpdate=r,e.shared.pending=null}while(1);null===n&&(k=l);e.baseState=k;e.firstBaseUpdate=m;e.lastBaseUpdate=n;b=e.shared.interleaved;if(null!==b){e=b;do g|=e.lane,e=e.next;while(e!==b)}else null===f&&(e.shared.lanes=0);ra|=g;a.lanes=g;a.memoizedState=l}}function Fh(a,b,c){a=b.effects;b.effects=null;if(null!==a)for(b=0;b<a.length;b++){var d=a[b],e=d.callback;if(null!==e){d.callback=null;d=c;if("function"!==typeof e)throw Error(n(191,
|
||||
e));e.call(d)}}}function ef(a,b,c,d){b=a.memoizedState;c=c(d,b);c=null===c||void 0===c?b:E({},b,c);a.memoizedState=c;0===a.lanes&&(a.updateQueue.baseState=c)}function Gh(a,b,c,d,e,f,g){a=a.stateNode;return"function"===typeof a.shouldComponentUpdate?a.shouldComponentUpdate(d,f,g):b.prototype&&b.prototype.isPureReactComponent?!qc(c,d)||!qc(e,f):!0}function Hh(a,b,c){var d=!1,e=cb;var f=b.contextType;"object"===typeof f&&null!==f?f=qa(f):(e=ea(b)?qb:J.current,d=b.contextTypes,f=(d=null!==d&&void 0!==
|
||||
d)?Nb(a,e):cb);b=new b(c,f);a.memoizedState=null!==b.state&&void 0!==b.state?b.state:null;b.updater=ud;a.stateNode=b;b._reactInternals=a;d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=e,a.__reactInternalMemoizedMaskedChildContext=f);return b}function Ih(a,b,c,d){a=b.state;"function"===typeof b.componentWillReceiveProps&&b.componentWillReceiveProps(c,d);"function"===typeof b.UNSAFE_componentWillReceiveProps&&b.UNSAFE_componentWillReceiveProps(c,d);b.state!==a&&ud.enqueueReplaceState(b,
|
||||
b.state,null)}function ff(a,b,c,d){var e=a.stateNode;e.props=c;e.state=a.memoizedState;e.refs=Jh;df(a);var f=b.contextType;"object"===typeof f&&null!==f?e.context=qa(f):(f=ea(b)?qb:J.current,e.context=Nb(a,f));e.state=a.memoizedState;f=b.getDerivedStateFromProps;"function"===typeof f&&(ef(a,b,f,c),e.state=a.memoizedState);"function"===typeof b.getDerivedStateFromProps||"function"===typeof e.getSnapshotBeforeUpdate||"function"!==typeof e.UNSAFE_componentWillMount&&"function"!==typeof e.componentWillMount||
|
||||
(b=e.state,"function"===typeof e.componentWillMount&&e.componentWillMount(),"function"===typeof e.UNSAFE_componentWillMount&&e.UNSAFE_componentWillMount(),b!==e.state&&ud.enqueueReplaceState(e,e.state,null),td(a,c,e,d),e.state=a.memoizedState);"function"===typeof e.componentDidMount&&(a.flags|=4194308)}function vc(a,b,c){a=c.ref;if(null!==a&&"function"!==typeof a&&"object"!==typeof a){if(c._owner){c=c._owner;if(c){if(1!==c.tag)throw Error(n(309));var d=c.stateNode}if(!d)throw Error(n(147,a));var e=
|
||||
d,f=""+a;if(null!==b&&null!==b.ref&&"function"===typeof b.ref&&b.ref._stringRef===f)return b.ref;b=function(a){var b=e.refs;b===Jh&&(b=e.refs={});null===a?delete b[f]:b[f]=a};b._stringRef=f;return b}if("string"!==typeof a)throw Error(n(284));if(!c._owner)throw Error(n(290,a));}return a}function vd(a,b){a=Object.prototype.toString.call(b);throw Error(n(31,"[object Object]"===a?"object with keys {"+Object.keys(b).join(", ")+"}":a));}function Kh(a){var b=a._init;return b(a._payload)}function Lh(a){function b(b,
|
||||
c){if(a){var d=b.deletions;null===d?(b.deletions=[c],b.flags|=16):d.push(c)}}function c(c,d){if(!a)return null;for(;null!==d;)b(c,d),d=d.sibling;return null}function d(a,b){for(a=new Map;null!==b;)null!==b.key?a.set(b.key,b):a.set(b.index,b),b=b.sibling;return a}function e(a,b){a=gb(a,b);a.index=0;a.sibling=null;return a}function f(b,c,d){b.index=d;if(!a)return b.flags|=1048576,c;d=b.alternate;if(null!==d)return d=d.index,d<c?(b.flags|=2,c):d;b.flags|=2;return c}function g(b){a&&null===b.alternate&&
|
||||
(b.flags|=2);return b}function h(a,b,c,d){if(null===b||6!==b.tag)return b=gf(c,a.mode,d),b.return=a,b;b=e(b,c);b.return=a;return b}function k(a,b,c,d){var f=c.type;if(f===Bb)return l(a,b,c.props.children,d,c.key);if(null!==b&&(b.elementType===f||"object"===typeof f&&null!==f&&f.$$typeof===Ta&&Kh(f)===b.type))return d=e(b,c.props),d.ref=vc(a,b,c),d.return=a,d;d=wd(c.type,c.key,c.props,null,a.mode,d);d.ref=vc(a,b,c);d.return=a;return d}function m(a,b,c,d){if(null===b||4!==b.tag||b.stateNode.containerInfo!==
|
||||
c.containerInfo||b.stateNode.implementation!==c.implementation)return b=hf(c,a.mode,d),b.return=a,b;b=e(b,c.children||[]);b.return=a;return b}function l(a,b,c,d,f){if(null===b||7!==b.tag)return b=ub(c,a.mode,d,f),b.return=a,b;b=e(b,c);b.return=a;return b}function u(a,b,c){if("string"===typeof b&&""!==b||"number"===typeof b)return b=gf(""+b,a.mode,c),b.return=a,b;if("object"===typeof b&&null!==b){switch(b.$$typeof){case xd:return c=wd(b.type,b.key,b.props,null,a.mode,c),c.ref=vc(a,null,b),c.return=
|
||||
a,c;case Cb:return b=hf(b,a.mode,c),b.return=a,b;case Ta:var d=b._init;return u(a,d(b._payload),c)}if(cc(b)||ac(b))return b=ub(b,a.mode,c,null),b.return=a,b;vd(a,b)}return null}function r(a,b,c,d){var e=null!==b?b.key:null;if("string"===typeof c&&""!==c||"number"===typeof c)return null!==e?null:h(a,b,""+c,d);if("object"===typeof c&&null!==c){switch(c.$$typeof){case xd:return c.key===e?k(a,b,c,d):null;case Cb:return c.key===e?m(a,b,c,d):null;case Ta:return e=c._init,r(a,b,e(c._payload),d)}if(cc(c)||
|
||||
ac(c))return null!==e?null:l(a,b,c,d,null);vd(a,c)}return null}function p(a,b,c,d,e){if("string"===typeof d&&""!==d||"number"===typeof d)return a=a.get(c)||null,h(b,a,""+d,e);if("object"===typeof d&&null!==d){switch(d.$$typeof){case xd:return a=a.get(null===d.key?c:d.key)||null,k(b,a,d,e);case Cb:return a=a.get(null===d.key?c:d.key)||null,m(b,a,d,e);case Ta:var f=d._init;return p(a,b,c,f(d._payload),e)}if(cc(d)||ac(d))return a=a.get(c)||null,l(b,a,d,e,null);vd(b,d)}return null}function x(e,g,h,k){for(var n=
|
||||
null,m=null,l=g,q=g=0,v=null;null!==l&&q<h.length;q++){l.index>q?(v=l,l=null):v=l.sibling;var A=r(e,l,h[q],k);if(null===A){null===l&&(l=v);break}a&&l&&null===A.alternate&&b(e,l);g=f(A,g,q);null===m?n=A:m.sibling=A;m=A;l=v}if(q===h.length)return c(e,l),D&&rb(e,q),n;if(null===l){for(;q<h.length;q++)l=u(e,h[q],k),null!==l&&(g=f(l,g,q),null===m?n=l:m.sibling=l,m=l);D&&rb(e,q);return n}for(l=d(e,l);q<h.length;q++)v=p(l,e,q,h[q],k),null!==v&&(a&&null!==v.alternate&&l.delete(null===v.key?q:v.key),g=f(v,
|
||||
g,q),null===m?n=v:m.sibling=v,m=v);a&&l.forEach(function(a){return b(e,a)});D&&rb(e,q);return n}function F(e,g,h,k){var m=ac(h);if("function"!==typeof m)throw Error(n(150));h=m.call(h);if(null==h)throw Error(n(151));for(var l=m=null,q=g,v=g=0,A=null,t=h.next();null!==q&&!t.done;v++,t=h.next()){q.index>v?(A=q,q=null):A=q.sibling;var x=r(e,q,t.value,k);if(null===x){null===q&&(q=A);break}a&&q&&null===x.alternate&&b(e,q);g=f(x,g,v);null===l?m=x:l.sibling=x;l=x;q=A}if(t.done)return c(e,q),D&&rb(e,v),m;
|
||||
if(null===q){for(;!t.done;v++,t=h.next())t=u(e,t.value,k),null!==t&&(g=f(t,g,v),null===l?m=t:l.sibling=t,l=t);D&&rb(e,v);return m}for(q=d(e,q);!t.done;v++,t=h.next())t=p(q,e,v,t.value,k),null!==t&&(a&&null!==t.alternate&&q.delete(null===t.key?v:t.key),g=f(t,g,v),null===l?m=t:l.sibling=t,l=t);a&&q.forEach(function(a){return b(e,a)});D&&rb(e,v);return m}function w(a,d,f,h){"object"===typeof f&&null!==f&&f.type===Bb&&null===f.key&&(f=f.props.children);if("object"===typeof f&&null!==f){switch(f.$$typeof){case xd:a:{for(var k=
|
||||
f.key,m=d;null!==m;){if(m.key===k){k=f.type;if(k===Bb){if(7===m.tag){c(a,m.sibling);d=e(m,f.props.children);d.return=a;a=d;break a}}else if(m.elementType===k||"object"===typeof k&&null!==k&&k.$$typeof===Ta&&Kh(k)===m.type){c(a,m.sibling);d=e(m,f.props);d.ref=vc(a,m,f);d.return=a;a=d;break a}c(a,m);break}else b(a,m);m=m.sibling}f.type===Bb?(d=ub(f.props.children,a.mode,h,f.key),d.return=a,a=d):(h=wd(f.type,f.key,f.props,null,a.mode,h),h.ref=vc(a,d,f),h.return=a,a=h)}return g(a);case Cb:a:{for(m=f.key;null!==
|
||||
d;){if(d.key===m)if(4===d.tag&&d.stateNode.containerInfo===f.containerInfo&&d.stateNode.implementation===f.implementation){c(a,d.sibling);d=e(d,f.children||[]);d.return=a;a=d;break a}else{c(a,d);break}else b(a,d);d=d.sibling}d=hf(f,a.mode,h);d.return=a;a=d}return g(a);case Ta:return m=f._init,w(a,d,m(f._payload),h)}if(cc(f))return x(a,d,f,h);if(ac(f))return F(a,d,f,h);vd(a,f)}return"string"===typeof f&&""!==f||"number"===typeof f?(f=""+f,null!==d&&6===d.tag?(c(a,d.sibling),d=e(d,f),d.return=a,a=d):
|
||||
(c(a,d),d=gf(f,a.mode,h),d.return=a,a=d),g(a)):c(a,d)}return w}function vb(a){if(a===wc)throw Error(n(174));return a}function jf(a,b){y(xc,b);y(yc,a);y(Ea,wc);a=b.nodeType;switch(a){case 9:case 11:b=(b=b.documentElement)?b.namespaceURI:oe(null,"");break;default:a=8===a?b.parentNode:b,b=a.namespaceURI||null,a=a.tagName,b=oe(b,a)}w(Ea);y(Ea,b)}function Tb(a){w(Ea);w(yc);w(xc)}function Mh(a){vb(xc.current);var b=vb(Ea.current);var c=oe(b,a.type);b!==c&&(y(yc,a),y(Ea,c))}function kf(a){yc.current===a&&
|
||||
(w(Ea),w(yc))}function yd(a){for(var b=a;null!==b;){if(13===b.tag){var c=b.memoizedState;if(null!==c&&(c=c.dehydrated,null===c||"$?"===c.data||"$!"===c.data))return b}else if(19===b.tag&&void 0!==b.memoizedProps.revealOrder){if(0!==(b.flags&128))return b}else if(null!==b.child){b.child.return=b;b=b.child;continue}if(b===a)break;for(;null===b.sibling;){if(null===b.return||b.return===a)return null;b=b.return}b.sibling.return=b.return;b=b.sibling}return null}function lf(){for(var a=0;a<mf.length;a++)mf[a]._workInProgressVersionPrimary=
|
||||
null;mf.length=0}function V(){throw Error(n(321));}function nf(a,b){if(null===b)return!1;for(var c=0;c<b.length&&c<a.length;c++)if(!ua(a[c],b[c]))return!1;return!0}function of(a,b,c,d,e,f){wb=f;C=b;b.memoizedState=null;b.updateQueue=null;b.lanes=0;zd.current=null===a||null===a.memoizedState?mk:nk;a=c(d,e);if(zc){f=0;do{zc=!1;Ac=0;if(25<=f)throw Error(n(301));f+=1;N=K=null;b.updateQueue=null;zd.current=ok;a=c(d,e)}while(zc)}zd.current=Ad;b=null!==K&&null!==K.next;wb=0;N=K=C=null;Bd=!1;if(b)throw Error(n(300));
|
||||
return a}function pf(){var a=0!==Ac;Ac=0;return a}function Fa(){var a={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};null===N?C.memoizedState=N=a:N=N.next=a;return N}function sa(){if(null===K){var a=C.alternate;a=null!==a?a.memoizedState:null}else a=K.next;var b=null===N?C.memoizedState:N.next;if(null!==b)N=b,K=a;else{if(null===a)throw Error(n(310));K=a;a={memoizedState:K.memoizedState,baseState:K.baseState,baseQueue:K.baseQueue,queue:K.queue,next:null};null===N?C.memoizedState=
|
||||
N=a:N=N.next=a}return N}function Bc(a,b){return"function"===typeof b?b(a):b}function qf(a,b,c){b=sa();c=b.queue;if(null===c)throw Error(n(311));c.lastRenderedReducer=a;var d=K,e=d.baseQueue,f=c.pending;if(null!==f){if(null!==e){var g=e.next;e.next=f.next;f.next=g}d.baseQueue=e=f;c.pending=null}if(null!==e){f=e.next;d=d.baseState;var h=g=null,k=null,m=f;do{var l=m.lane;if((wb&l)===l)null!==k&&(k=k.next={lane:0,action:m.action,hasEagerState:m.hasEagerState,eagerState:m.eagerState,next:null}),d=m.hasEagerState?
|
||||
m.eagerState:a(d,m.action);else{var u={lane:l,action:m.action,hasEagerState:m.hasEagerState,eagerState:m.eagerState,next:null};null===k?(h=k=u,g=d):k=k.next=u;C.lanes|=l;ra|=l}m=m.next}while(null!==m&&m!==f);null===k?g=d:k.next=h;ua(d,b.memoizedState)||(ha=!0);b.memoizedState=d;b.baseState=g;b.baseQueue=k;c.lastRenderedState=d}a=c.interleaved;if(null!==a){e=a;do f=e.lane,C.lanes|=f,ra|=f,e=e.next;while(e!==a)}else null===e&&(c.lanes=0);return[b.memoizedState,c.dispatch]}function rf(a,b,c){b=sa();
|
||||
c=b.queue;if(null===c)throw Error(n(311));c.lastRenderedReducer=a;var d=c.dispatch,e=c.pending,f=b.memoizedState;if(null!==e){c.pending=null;var g=e=e.next;do f=a(f,g.action),g=g.next;while(g!==e);ua(f,b.memoizedState)||(ha=!0);b.memoizedState=f;null===b.baseQueue&&(b.baseState=f);c.lastRenderedState=f}return[f,d]}function Nh(a,b,c){}function Oh(a,b,c){c=C;var d=sa(),e=b(),f=!ua(d.memoizedState,e);f&&(d.memoizedState=e,ha=!0);d=d.queue;sf(Ph.bind(null,c,d,a),[a]);if(d.getSnapshot!==b||f||null!==N&&
|
||||
N.memoizedState.tag&1){c.flags|=2048;Cc(9,Qh.bind(null,c,d,e,b),void 0,null);if(null===O)throw Error(n(349));0!==(wb&30)||Rh(c,b,e)}return e}function Rh(a,b,c){a.flags|=16384;a={getSnapshot:b,value:c};b=C.updateQueue;null===b?(b={lastEffect:null,stores:null},C.updateQueue=b,b.stores=[a]):(c=b.stores,null===c?b.stores=[a]:c.push(a))}function Qh(a,b,c,d){b.value=c;b.getSnapshot=d;Sh(b)&&Th(a)}function Ph(a,b,c){return c(function(){Sh(b)&&Th(a)})}function Sh(a){var b=a.getSnapshot;a=a.value;try{var c=
|
||||
b();return!ua(a,c)}catch(d){return!0}}function Th(a){var b=Oa(a,1);null!==b&&ya(b,a,1,-1)}function Uh(a){var b=Fa();"function"===typeof a&&(a=a());b.memoizedState=b.baseState=a;a={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:Bc,lastRenderedState:a};b.queue=a;a=a.dispatch=pk.bind(null,C,a);return[b.memoizedState,a]}function Cc(a,b,c,d){a={tag:a,create:b,destroy:c,deps:d,next:null};b=C.updateQueue;null===b?(b={lastEffect:null,stores:null},C.updateQueue=b,b.lastEffect=a.next=
|
||||
a):(c=b.lastEffect,null===c?b.lastEffect=a.next=a:(d=c.next,c.next=a,a.next=d,b.lastEffect=a));return a}function Vh(a){return sa().memoizedState}function Cd(a,b,c,d){var e=Fa();C.flags|=a;e.memoizedState=Cc(1|b,c,void 0,void 0===d?null:d)}function Dd(a,b,c,d){var e=sa();d=void 0===d?null:d;var f=void 0;if(null!==K){var g=K.memoizedState;f=g.destroy;if(null!==d&&nf(d,g.deps)){e.memoizedState=Cc(b,c,f,d);return}}C.flags|=a;e.memoizedState=Cc(1|b,c,f,d)}function Wh(a,b){return Cd(8390656,8,a,b)}function sf(a,
|
||||
b){return Dd(2048,8,a,b)}function Xh(a,b){return Dd(4,2,a,b)}function Yh(a,b){return Dd(4,4,a,b)}function Zh(a,b){if("function"===typeof b)return a=a(),b(a),function(){b(null)};if(null!==b&&void 0!==b)return a=a(),b.current=a,function(){b.current=null}}function $h(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;return Dd(4,4,Zh.bind(null,b,a),c)}function tf(a,b){}function ai(a,b){var c=sa();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&nf(b,d[1]))return d[0];c.memoizedState=[a,
|
||||
b];return a}function bi(a,b){var c=sa();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&nf(b,d[1]))return d[0];a=a();c.memoizedState=[a,b];return a}function ci(a,b,c){if(0===(wb&21))return a.baseState&&(a.baseState=!1,ha=!0),a.memoizedState=c;ua(c,b)||(c=Dg(),C.lanes|=c,ra|=c,a.baseState=!0);return b}function qk(a,b,c){c=z;z=0!==c&&4>c?c:4;a(!0);var d=uf.transition;uf.transition={};try{a(!1),b()}finally{z=c,uf.transition=d}}function di(){return sa().memoizedState}function rk(a,b,
|
||||
c){var d=hb(a);c={lane:d,action:c,hasEagerState:!1,eagerState:null,next:null};if(ei(a))fi(b,c);else if(c=Ch(a,b,c,d),null!==c){var e=Z();ya(c,a,d,e);gi(c,b,d)}}function pk(a,b,c){var d=hb(a),e={lane:d,action:c,hasEagerState:!1,eagerState:null,next:null};if(ei(a))fi(b,e);else{var f=a.alternate;if(0===a.lanes&&(null===f||0===f.lanes)&&(f=b.lastRenderedReducer,null!==f))try{var g=b.lastRenderedState,h=f(g,c);e.hasEagerState=!0;e.eagerState=h;if(ua(h,g)){var k=b.interleaved;null===k?(e.next=e,cf(b)):
|
||||
(e.next=k.next,k.next=e);b.interleaved=e;return}}catch(m){}finally{}c=Ch(a,b,e,d);null!==c&&(e=Z(),ya(c,a,d,e),gi(c,b,d))}}function ei(a){var b=a.alternate;return a===C||null!==b&&b===C}function fi(a,b){zc=Bd=!0;var c=a.pending;null===c?b.next=b:(b.next=c.next,c.next=b);a.pending=b}function gi(a,b,c){if(0!==(c&4194240)){var d=b.lanes;d&=a.pendingLanes;c|=d;b.lanes=c;xe(a,c)}}function Ub(a,b){try{var c="",d=b;do c+=gj(d),d=d.return;while(d);var e=c}catch(f){e="\nError generating stack: "+f.message+
|
||||
"\n"+f.stack}return{value:a,source:b,stack:e,digest:null}}function vf(a,b,c){return{value:a,source:null,stack:null!=c?c:null,digest:null!=b?b:null}}function wf(a,b){try{console.error(b.value)}catch(c){setTimeout(function(){throw c;})}}function hi(a,b,c){c=Pa(-1,c);c.tag=3;c.payload={element:null};var d=b.value;c.callback=function(){Ed||(Ed=!0,xf=d);wf(a,b)};return c}function ii(a,b,c){c=Pa(-1,c);c.tag=3;var d=a.type.getDerivedStateFromError;if("function"===typeof d){var e=b.value;c.payload=function(){return d(e)};
|
||||
c.callback=function(){wf(a,b)}}var f=a.stateNode;null!==f&&"function"===typeof f.componentDidCatch&&(c.callback=function(){wf(a,b);"function"!==typeof d&&(null===ib?ib=new Set([this]):ib.add(this));var c=b.stack;this.componentDidCatch(b.value,{componentStack:null!==c?c:""})});return c}function ji(a,b,c){var d=a.pingCache;if(null===d){d=a.pingCache=new sk;var e=new Set;d.set(b,e)}else e=d.get(b),void 0===e&&(e=new Set,d.set(b,e));e.has(c)||(e.add(c),a=tk.bind(null,a,b,c),b.then(a,a))}function ki(a){do{var b;
|
||||
if(b=13===a.tag)b=a.memoizedState,b=null!==b?null!==b.dehydrated?!0:!1:!0;if(b)return a;a=a.return}while(null!==a);return null}function li(a,b,c,d,e){if(0===(a.mode&1))return a===b?a.flags|=65536:(a.flags|=128,c.flags|=131072,c.flags&=-52805,1===c.tag&&(null===c.alternate?c.tag=17:(b=Pa(-1,1),b.tag=2,eb(c,b,1))),c.lanes|=1),a;a.flags|=65536;a.lanes=e;return a}function aa(a,b,c,d){b.child=null===a?mi(b,null,c,d):Vb(b,a.child,c,d)}function ni(a,b,c,d,e){c=c.render;var f=b.ref;Sb(b,e);d=of(a,b,c,d,f,
|
||||
e);c=pf();if(null!==a&&!ha)return b.updateQueue=a.updateQueue,b.flags&=-2053,a.lanes&=~e,Qa(a,b,e);D&&c&&Ue(b);b.flags|=1;aa(a,b,d,e);return b.child}function oi(a,b,c,d,e){if(null===a){var f=c.type;if("function"===typeof f&&!yf(f)&&void 0===f.defaultProps&&null===c.compare&&void 0===c.defaultProps)return b.tag=15,b.type=f,pi(a,b,f,d,e);a=wd(c.type,null,d,b,b.mode,e);a.ref=b.ref;a.return=b;return b.child=a}f=a.child;if(0===(a.lanes&e)){var g=f.memoizedProps;c=c.compare;c=null!==c?c:qc;if(c(g,d)&&a.ref===
|
||||
b.ref)return Qa(a,b,e)}b.flags|=1;a=gb(f,d);a.ref=b.ref;a.return=b;return b.child=a}function pi(a,b,c,d,e){if(null!==a){var f=a.memoizedProps;if(qc(f,d)&&a.ref===b.ref)if(ha=!1,b.pendingProps=d=f,0!==(a.lanes&e))0!==(a.flags&131072)&&(ha=!0);else return b.lanes=a.lanes,Qa(a,b,e)}return zf(a,b,c,d,e)}function qi(a,b,c){var d=b.pendingProps,e=d.children,f=null!==a?a.memoizedState:null;if("hidden"===d.mode)if(0===(b.mode&1))b.memoizedState={baseLanes:0,cachePool:null,transitions:null},y(Ga,ba),ba|=c;
|
||||
else{if(0===(c&1073741824))return a=null!==f?f.baseLanes|c:c,b.lanes=b.childLanes=1073741824,b.memoizedState={baseLanes:a,cachePool:null,transitions:null},b.updateQueue=null,y(Ga,ba),ba|=a,null;b.memoizedState={baseLanes:0,cachePool:null,transitions:null};d=null!==f?f.baseLanes:c;y(Ga,ba);ba|=d}else null!==f?(d=f.baseLanes|c,b.memoizedState=null):d=c,y(Ga,ba),ba|=d;aa(a,b,e,c);return b.child}function ri(a,b){var c=b.ref;if(null===a&&null!==c||null!==a&&a.ref!==c)b.flags|=512,b.flags|=2097152}function zf(a,
|
||||
b,c,d,e){var f=ea(c)?qb:J.current;f=Nb(b,f);Sb(b,e);c=of(a,b,c,d,f,e);d=pf();if(null!==a&&!ha)return b.updateQueue=a.updateQueue,b.flags&=-2053,a.lanes&=~e,Qa(a,b,e);D&&d&&Ue(b);b.flags|=1;aa(a,b,c,e);return b.child}function si(a,b,c,d,e){if(ea(c)){var f=!0;ld(b)}else f=!1;Sb(b,e);if(null===b.stateNode)Fd(a,b),Hh(b,c,d),ff(b,c,d,e),d=!0;else if(null===a){var g=b.stateNode,h=b.memoizedProps;g.props=h;var k=g.context,m=c.contextType;"object"===typeof m&&null!==m?m=qa(m):(m=ea(c)?qb:J.current,m=Nb(b,
|
||||
m));var l=c.getDerivedStateFromProps,n="function"===typeof l||"function"===typeof g.getSnapshotBeforeUpdate;n||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==d||k!==m)&&Ih(b,g,d,m);fb=!1;var r=b.memoizedState;g.state=r;td(b,d,g,e);k=b.memoizedState;h!==d||r!==k||S.current||fb?("function"===typeof l&&(ef(b,c,l,d),k=b.memoizedState),(h=fb||Gh(b,c,h,d,r,k,m))?(n||"function"!==typeof g.UNSAFE_componentWillMount&&"function"!==typeof g.componentWillMount||
|
||||
("function"===typeof g.componentWillMount&&g.componentWillMount(),"function"===typeof g.UNSAFE_componentWillMount&&g.UNSAFE_componentWillMount()),"function"===typeof g.componentDidMount&&(b.flags|=4194308)):("function"===typeof g.componentDidMount&&(b.flags|=4194308),b.memoizedProps=d,b.memoizedState=k),g.props=d,g.state=k,g.context=m,d=h):("function"===typeof g.componentDidMount&&(b.flags|=4194308),d=!1)}else{g=b.stateNode;Dh(a,b);h=b.memoizedProps;m=b.type===b.elementType?h:xa(b.type,h);g.props=
|
||||
m;n=b.pendingProps;r=g.context;k=c.contextType;"object"===typeof k&&null!==k?k=qa(k):(k=ea(c)?qb:J.current,k=Nb(b,k));var p=c.getDerivedStateFromProps;(l="function"===typeof p||"function"===typeof g.getSnapshotBeforeUpdate)||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==n||r!==k)&&Ih(b,g,d,k);fb=!1;r=b.memoizedState;g.state=r;td(b,d,g,e);var x=b.memoizedState;h!==n||r!==x||S.current||fb?("function"===typeof p&&(ef(b,c,p,d),x=b.memoizedState),
|
||||
(m=fb||Gh(b,c,m,d,r,x,k)||!1)?(l||"function"!==typeof g.UNSAFE_componentWillUpdate&&"function"!==typeof g.componentWillUpdate||("function"===typeof g.componentWillUpdate&&g.componentWillUpdate(d,x,k),"function"===typeof g.UNSAFE_componentWillUpdate&&g.UNSAFE_componentWillUpdate(d,x,k)),"function"===typeof g.componentDidUpdate&&(b.flags|=4),"function"===typeof g.getSnapshotBeforeUpdate&&(b.flags|=1024)):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&r===a.memoizedState||(b.flags|=
|
||||
4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&r===a.memoizedState||(b.flags|=1024),b.memoizedProps=d,b.memoizedState=x),g.props=d,g.state=x,g.context=k,d=m):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&r===a.memoizedState||(b.flags|=4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&r===a.memoizedState||(b.flags|=1024),d=!1)}return Af(a,b,c,d,f,e)}function Af(a,b,c,d,e,f){ri(a,b);var g=0!==(b.flags&128);if(!d&&!g)return e&&vh(b,c,!1),
|
||||
Qa(a,b,f);d=b.stateNode;uk.current=b;var h=g&&"function"!==typeof c.getDerivedStateFromError?null:d.render();b.flags|=1;null!==a&&g?(b.child=Vb(b,a.child,null,f),b.child=Vb(b,null,h,f)):aa(a,b,h,f);b.memoizedState=d.state;e&&vh(b,c,!0);return b.child}function ti(a){var b=a.stateNode;b.pendingContext?th(a,b.pendingContext,b.pendingContext!==b.context):b.context&&th(a,b.context,!1);jf(a,b.containerInfo)}function ui(a,b,c,d,e){Qb();Ye(e);b.flags|=256;aa(a,b,c,d);return b.child}function Bf(a){return{baseLanes:a,
|
||||
cachePool:null,transitions:null}}function vi(a,b,c){var d=b.pendingProps,e=G.current,f=!1,g=0!==(b.flags&128),h;(h=g)||(h=null!==a&&null===a.memoizedState?!1:0!==(e&2));if(h)f=!0,b.flags&=-129;else if(null===a||null!==a.memoizedState)e|=1;y(G,e&1);if(null===a){Xe(b);a=b.memoizedState;if(null!==a&&(a=a.dehydrated,null!==a))return 0===(b.mode&1)?b.lanes=1:"$!"===a.data?b.lanes=8:b.lanes=1073741824,null;g=d.children;a=d.fallback;return f?(d=b.mode,f=b.child,g={mode:"hidden",children:g},0===(d&1)&&null!==
|
||||
f?(f.childLanes=0,f.pendingProps=g):f=Gd(g,d,0,null),a=ub(a,d,c,null),f.return=b,a.return=b,f.sibling=a,b.child=f,b.child.memoizedState=Bf(c),b.memoizedState=Cf,a):Df(b,g)}e=a.memoizedState;if(null!==e&&(h=e.dehydrated,null!==h))return vk(a,b,g,d,h,e,c);if(f){f=d.fallback;g=b.mode;e=a.child;h=e.sibling;var k={mode:"hidden",children:d.children};0===(g&1)&&b.child!==e?(d=b.child,d.childLanes=0,d.pendingProps=k,b.deletions=null):(d=gb(e,k),d.subtreeFlags=e.subtreeFlags&14680064);null!==h?f=gb(h,f):(f=
|
||||
ub(f,g,c,null),f.flags|=2);f.return=b;d.return=b;d.sibling=f;b.child=d;d=f;f=b.child;g=a.child.memoizedState;g=null===g?Bf(c):{baseLanes:g.baseLanes|c,cachePool:null,transitions:g.transitions};f.memoizedState=g;f.childLanes=a.childLanes&~c;b.memoizedState=Cf;return d}f=a.child;a=f.sibling;d=gb(f,{mode:"visible",children:d.children});0===(b.mode&1)&&(d.lanes=c);d.return=b;d.sibling=null;null!==a&&(c=b.deletions,null===c?(b.deletions=[a],b.flags|=16):c.push(a));b.child=d;b.memoizedState=null;return d}
|
||||
function Df(a,b,c){b=Gd({mode:"visible",children:b},a.mode,0,null);b.return=a;return a.child=b}function Hd(a,b,c,d){null!==d&&Ye(d);Vb(b,a.child,null,c);a=Df(b,b.pendingProps.children);a.flags|=2;b.memoizedState=null;return a}function vk(a,b,c,d,e,f,g){if(c){if(b.flags&256)return b.flags&=-257,d=vf(Error(n(422))),Hd(a,b,g,d);if(null!==b.memoizedState)return b.child=a.child,b.flags|=128,null;f=d.fallback;e=b.mode;d=Gd({mode:"visible",children:d.children},e,0,null);f=ub(f,e,g,null);f.flags|=2;d.return=
|
||||
b;f.return=b;d.sibling=f;b.child=d;0!==(b.mode&1)&&Vb(b,a.child,null,g);b.child.memoizedState=Bf(g);b.memoizedState=Cf;return f}if(0===(b.mode&1))return Hd(a,b,g,null);if("$!"===e.data){d=e.nextSibling&&e.nextSibling.dataset;if(d)var h=d.dgst;d=h;f=Error(n(419));d=vf(f,d,void 0);return Hd(a,b,g,d)}h=0!==(g&a.childLanes);if(ha||h){d=O;if(null!==d){switch(g&-g){case 4:e=2;break;case 16:e=8;break;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:e=
|
||||
32;break;case 536870912:e=268435456;break;default:e=0}e=0!==(e&(d.suspendedLanes|g))?0:e;0!==e&&e!==f.retryLane&&(f.retryLane=e,Oa(a,e),ya(d,a,e,-1))}Ef();d=vf(Error(n(421)));return Hd(a,b,g,d)}if("$?"===e.data)return b.flags|=128,b.child=a.child,b=wk.bind(null,a),e._reactRetry=b,null;a=f.treeContext;fa=Ka(e.nextSibling);la=b;D=!0;wa=null;null!==a&&(na[oa++]=Ma,na[oa++]=Na,na[oa++]=sb,Ma=a.id,Na=a.overflow,sb=b);b=Df(b,d.children);b.flags|=4096;return b}function wi(a,b,c){a.lanes|=b;var d=a.alternate;
|
||||
null!==d&&(d.lanes|=b);bf(a.return,b,c)}function Ff(a,b,c,d,e){var f=a.memoizedState;null===f?a.memoizedState={isBackwards:b,rendering:null,renderingStartTime:0,last:d,tail:c,tailMode:e}:(f.isBackwards=b,f.rendering=null,f.renderingStartTime=0,f.last=d,f.tail=c,f.tailMode=e)}function xi(a,b,c){var d=b.pendingProps,e=d.revealOrder,f=d.tail;aa(a,b,d.children,c);d=G.current;if(0!==(d&2))d=d&1|2,b.flags|=128;else{if(null!==a&&0!==(a.flags&128))a:for(a=b.child;null!==a;){if(13===a.tag)null!==a.memoizedState&&
|
||||
wi(a,c,b);else if(19===a.tag)wi(a,c,b);else if(null!==a.child){a.child.return=a;a=a.child;continue}if(a===b)break a;for(;null===a.sibling;){if(null===a.return||a.return===b)break a;a=a.return}a.sibling.return=a.return;a=a.sibling}d&=1}y(G,d);if(0===(b.mode&1))b.memoizedState=null;else switch(e){case "forwards":c=b.child;for(e=null;null!==c;)a=c.alternate,null!==a&&null===yd(a)&&(e=c),c=c.sibling;c=e;null===c?(e=b.child,b.child=null):(e=c.sibling,c.sibling=null);Ff(b,!1,e,c,f);break;case "backwards":c=
|
||||
null;e=b.child;for(b.child=null;null!==e;){a=e.alternate;if(null!==a&&null===yd(a)){b.child=e;break}a=e.sibling;e.sibling=c;c=e;e=a}Ff(b,!0,c,null,f);break;case "together":Ff(b,!1,null,null,void 0);break;default:b.memoizedState=null}return b.child}function Fd(a,b){0===(b.mode&1)&&null!==a&&(a.alternate=null,b.alternate=null,b.flags|=2)}function Qa(a,b,c){null!==a&&(b.dependencies=a.dependencies);ra|=b.lanes;if(0===(c&b.childLanes))return null;if(null!==a&&b.child!==a.child)throw Error(n(153));if(null!==
|
||||
b.child){a=b.child;c=gb(a,a.pendingProps);b.child=c;for(c.return=b;null!==a.sibling;)a=a.sibling,c=c.sibling=gb(a,a.pendingProps),c.return=b;c.sibling=null}return b.child}function xk(a,b,c){switch(b.tag){case 3:ti(b);Qb();break;case 5:Mh(b);break;case 1:ea(b.type)&&ld(b);break;case 4:jf(b,b.stateNode.containerInfo);break;case 10:var d=b.type._context,e=b.memoizedProps.value;y(rd,d._currentValue);d._currentValue=e;break;case 13:d=b.memoizedState;if(null!==d){if(null!==d.dehydrated)return y(G,G.current&
|
||||
1),b.flags|=128,null;if(0!==(c&b.child.childLanes))return vi(a,b,c);y(G,G.current&1);a=Qa(a,b,c);return null!==a?a.sibling:null}y(G,G.current&1);break;case 19:d=0!==(c&b.childLanes);if(0!==(a.flags&128)){if(d)return xi(a,b,c);b.flags|=128}e=b.memoizedState;null!==e&&(e.rendering=null,e.tail=null,e.lastEffect=null);y(G,G.current);if(d)break;else return null;case 22:case 23:return b.lanes=0,qi(a,b,c)}return Qa(a,b,c)}function Dc(a,b){if(!D)switch(a.tailMode){case "hidden":b=a.tail;for(var c=null;null!==
|
||||
b;)null!==b.alternate&&(c=b),b=b.sibling;null===c?a.tail=null:c.sibling=null;break;case "collapsed":c=a.tail;for(var d=null;null!==c;)null!==c.alternate&&(d=c),c=c.sibling;null===d?b||null===a.tail?a.tail=null:a.tail.sibling=null:d.sibling=null}}function W(a){var b=null!==a.alternate&&a.alternate.child===a.child,c=0,d=0;if(b)for(var e=a.child;null!==e;)c|=e.lanes|e.childLanes,d|=e.subtreeFlags&14680064,d|=e.flags&14680064,e.return=a,e=e.sibling;else for(e=a.child;null!==e;)c|=e.lanes|e.childLanes,
|
||||
d|=e.subtreeFlags,d|=e.flags,e.return=a,e=e.sibling;a.subtreeFlags|=d;a.childLanes=c;return b}function yk(a,b,c){var d=b.pendingProps;Ve(b);switch(b.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return W(b),null;case 1:return ea(b.type)&&(w(S),w(J)),W(b),null;case 3:d=b.stateNode;Tb();w(S);w(J);lf();d.pendingContext&&(d.context=d.pendingContext,d.pendingContext=null);if(null===a||null===a.child)pd(b)?b.flags|=4:null===a||a.memoizedState.isDehydrated&&0===(b.flags&
|
||||
256)||(b.flags|=1024,null!==wa&&(Gf(wa),wa=null));yi(a,b);W(b);return null;case 5:kf(b);var e=vb(xc.current);c=b.type;if(null!==a&&null!=b.stateNode)zk(a,b,c,d,e),a.ref!==b.ref&&(b.flags|=512,b.flags|=2097152);else{if(!d){if(null===b.stateNode)throw Error(n(166));W(b);return null}a=vb(Ea.current);if(pd(b)){d=b.stateNode;c=b.type;var f=b.memoizedProps;d[Da]=b;d[uc]=f;a=0!==(b.mode&1);switch(c){case "dialog":B("cancel",d);B("close",d);break;case "iframe":case "object":case "embed":B("load",d);break;
|
||||
case "video":case "audio":for(e=0;e<Ec.length;e++)B(Ec[e],d);break;case "source":B("error",d);break;case "img":case "image":case "link":B("error",d);B("load",d);break;case "details":B("toggle",d);break;case "input":kg(d,f);B("invalid",d);break;case "select":d._wrapperState={wasMultiple:!!f.multiple};B("invalid",d);break;case "textarea":ng(d,f),B("invalid",d)}pe(c,f);e=null;for(var g in f)if(f.hasOwnProperty(g)){var h=f[g];"children"===g?"string"===typeof h?d.textContent!==h&&(!0!==f.suppressHydrationWarning&&
|
||||
jd(d.textContent,h,a),e=["children",h]):"number"===typeof h&&d.textContent!==""+h&&(!0!==f.suppressHydrationWarning&&jd(d.textContent,h,a),e=["children",""+h]):$b.hasOwnProperty(g)&&null!=h&&"onScroll"===g&&B("scroll",d)}switch(c){case "input":Pc(d);mg(d,f,!0);break;case "textarea":Pc(d);pg(d);break;case "select":case "option":break;default:"function"===typeof f.onClick&&(d.onclick=kd)}d=e;b.updateQueue=d;null!==d&&(b.flags|=4)}else{g=9===e.nodeType?e:e.ownerDocument;"http://www.w3.org/1999/xhtml"===
|
||||
a&&(a=qg(c));"http://www.w3.org/1999/xhtml"===a?"script"===c?(a=g.createElement("div"),a.innerHTML="<script>\x3c/script>",a=a.removeChild(a.firstChild)):"string"===typeof d.is?a=g.createElement(c,{is:d.is}):(a=g.createElement(c),"select"===c&&(g=a,d.multiple?g.multiple=!0:d.size&&(g.size=d.size))):a=g.createElementNS(a,c);a[Da]=b;a[uc]=d;Ak(a,b,!1,!1);b.stateNode=a;a:{g=qe(c,d);switch(c){case "dialog":B("cancel",a);B("close",a);e=d;break;case "iframe":case "object":case "embed":B("load",a);e=d;break;
|
||||
case "video":case "audio":for(e=0;e<Ec.length;e++)B(Ec[e],a);e=d;break;case "source":B("error",a);e=d;break;case "img":case "image":case "link":B("error",a);B("load",a);e=d;break;case "details":B("toggle",a);e=d;break;case "input":kg(a,d);e=ke(a,d);B("invalid",a);break;case "option":e=d;break;case "select":a._wrapperState={wasMultiple:!!d.multiple};e=E({},d,{value:void 0});B("invalid",a);break;case "textarea":ng(a,d);e=ne(a,d);B("invalid",a);break;default:e=d}pe(c,e);h=e;for(f in h)if(h.hasOwnProperty(f)){var k=
|
||||
h[f];"style"===f?sg(a,k):"dangerouslySetInnerHTML"===f?(k=k?k.__html:void 0,null!=k&&zi(a,k)):"children"===f?"string"===typeof k?("textarea"!==c||""!==k)&&Fc(a,k):"number"===typeof k&&Fc(a,""+k):"suppressContentEditableWarning"!==f&&"suppressHydrationWarning"!==f&&"autoFocus"!==f&&($b.hasOwnProperty(f)?null!=k&&"onScroll"===f&&B("scroll",a):null!=k&&$d(a,f,k,g))}switch(c){case "input":Pc(a);mg(a,d,!1);break;case "textarea":Pc(a);pg(a);break;case "option":null!=d.value&&a.setAttribute("value",""+Ua(d.value));
|
||||
break;case "select":a.multiple=!!d.multiple;f=d.value;null!=f?Db(a,!!d.multiple,f,!1):null!=d.defaultValue&&Db(a,!!d.multiple,d.defaultValue,!0);break;default:"function"===typeof e.onClick&&(a.onclick=kd)}switch(c){case "button":case "input":case "select":case "textarea":d=!!d.autoFocus;break a;case "img":d=!0;break a;default:d=!1}}d&&(b.flags|=4)}null!==b.ref&&(b.flags|=512,b.flags|=2097152)}W(b);return null;case 6:if(a&&null!=b.stateNode)Bk(a,b,a.memoizedProps,d);else{if("string"!==typeof d&&null===
|
||||
b.stateNode)throw Error(n(166));c=vb(xc.current);vb(Ea.current);if(pd(b)){d=b.stateNode;c=b.memoizedProps;d[Da]=b;if(f=d.nodeValue!==c)if(a=la,null!==a)switch(a.tag){case 3:jd(d.nodeValue,c,0!==(a.mode&1));break;case 5:!0!==a.memoizedProps.suppressHydrationWarning&&jd(d.nodeValue,c,0!==(a.mode&1))}f&&(b.flags|=4)}else d=(9===c.nodeType?c:c.ownerDocument).createTextNode(d),d[Da]=b,b.stateNode=d}W(b);return null;case 13:w(G);d=b.memoizedState;if(null===a||null!==a.memoizedState&&null!==a.memoizedState.dehydrated){if(D&&
|
||||
null!==fa&&0!==(b.mode&1)&&0===(b.flags&128)){for(f=fa;f;)f=Ka(f.nextSibling);Qb();b.flags|=98560;f=!1}else if(f=pd(b),null!==d&&null!==d.dehydrated){if(null===a){if(!f)throw Error(n(318));f=b.memoizedState;f=null!==f?f.dehydrated:null;if(!f)throw Error(n(317));f[Da]=b}else Qb(),0===(b.flags&128)&&(b.memoizedState=null),b.flags|=4;W(b);f=!1}else null!==wa&&(Gf(wa),wa=null),f=!0;if(!f)return b.flags&65536?b:null}if(0!==(b.flags&128))return b.lanes=c,b;d=null!==d;d!==(null!==a&&null!==a.memoizedState)&&
|
||||
d&&(b.child.flags|=8192,0!==(b.mode&1)&&(null===a||0!==(G.current&1)?0===L&&(L=3):Ef()));null!==b.updateQueue&&(b.flags|=4);W(b);return null;case 4:return Tb(),yi(a,b),null===a&&sc(b.stateNode.containerInfo),W(b),null;case 10:return af(b.type._context),W(b),null;case 17:return ea(b.type)&&(w(S),w(J)),W(b),null;case 19:w(G);f=b.memoizedState;if(null===f)return W(b),null;d=0!==(b.flags&128);g=f.rendering;if(null===g)if(d)Dc(f,!1);else{if(0!==L||null!==a&&0!==(a.flags&128))for(a=b.child;null!==a;){g=
|
||||
yd(a);if(null!==g){b.flags|=128;Dc(f,!1);d=g.updateQueue;null!==d&&(b.updateQueue=d,b.flags|=4);b.subtreeFlags=0;d=c;for(c=b.child;null!==c;)f=c,a=d,f.flags&=14680066,g=f.alternate,null===g?(f.childLanes=0,f.lanes=a,f.child=null,f.subtreeFlags=0,f.memoizedProps=null,f.memoizedState=null,f.updateQueue=null,f.dependencies=null,f.stateNode=null):(f.childLanes=g.childLanes,f.lanes=g.lanes,f.child=g.child,f.subtreeFlags=0,f.deletions=null,f.memoizedProps=g.memoizedProps,f.memoizedState=g.memoizedState,
|
||||
f.updateQueue=g.updateQueue,f.type=g.type,a=g.dependencies,f.dependencies=null===a?null:{lanes:a.lanes,firstContext:a.firstContext}),c=c.sibling;y(G,G.current&1|2);return b.child}a=a.sibling}null!==f.tail&&P()>Hf&&(b.flags|=128,d=!0,Dc(f,!1),b.lanes=4194304)}else{if(!d)if(a=yd(g),null!==a){if(b.flags|=128,d=!0,c=a.updateQueue,null!==c&&(b.updateQueue=c,b.flags|=4),Dc(f,!0),null===f.tail&&"hidden"===f.tailMode&&!g.alternate&&!D)return W(b),null}else 2*P()-f.renderingStartTime>Hf&&1073741824!==c&&(b.flags|=
|
||||
128,d=!0,Dc(f,!1),b.lanes=4194304);f.isBackwards?(g.sibling=b.child,b.child=g):(c=f.last,null!==c?c.sibling=g:b.child=g,f.last=g)}if(null!==f.tail)return b=f.tail,f.rendering=b,f.tail=b.sibling,f.renderingStartTime=P(),b.sibling=null,c=G.current,y(G,d?c&1|2:c&1),b;W(b);return null;case 22:case 23:return ba=Ga.current,w(Ga),d=null!==b.memoizedState,null!==a&&null!==a.memoizedState!==d&&(b.flags|=8192),d&&0!==(b.mode&1)?0!==(ba&1073741824)&&(W(b),b.subtreeFlags&6&&(b.flags|=8192)):W(b),null;case 24:return null;
|
||||
case 25:return null}throw Error(n(156,b.tag));}function Ck(a,b,c){Ve(b);switch(b.tag){case 1:return ea(b.type)&&(w(S),w(J)),a=b.flags,a&65536?(b.flags=a&-65537|128,b):null;case 3:return Tb(),w(S),w(J),lf(),a=b.flags,0!==(a&65536)&&0===(a&128)?(b.flags=a&-65537|128,b):null;case 5:return kf(b),null;case 13:w(G);a=b.memoizedState;if(null!==a&&null!==a.dehydrated){if(null===b.alternate)throw Error(n(340));Qb()}a=b.flags;return a&65536?(b.flags=a&-65537|128,b):null;case 19:return w(G),null;case 4:return Tb(),
|
||||
null;case 10:return af(b.type._context),null;case 22:case 23:return ba=Ga.current,w(Ga),null;case 24:return null;default:return null}}function Wb(a,b){var c=a.ref;if(null!==c)if("function"===typeof c)try{c(null)}catch(d){H(a,b,d)}else c.current=null}function If(a,b,c){try{c()}catch(d){H(a,b,d)}}function Dk(a,b){Jf=Zc;a=ch();if(Ie(a)){if("selectionStart"in a)var c={start:a.selectionStart,end:a.selectionEnd};else a:{c=(c=a.ownerDocument)&&c.defaultView||window;var d=c.getSelection&&c.getSelection();
|
||||
if(d&&0!==d.rangeCount){c=d.anchorNode;var e=d.anchorOffset,f=d.focusNode;d=d.focusOffset;try{c.nodeType,f.nodeType}catch(M){c=null;break a}var g=0,h=-1,k=-1,m=0,t=0,u=a,r=null;b:for(;;){for(var p;;){u!==c||0!==e&&3!==u.nodeType||(h=g+e);u!==f||0!==d&&3!==u.nodeType||(k=g+d);3===u.nodeType&&(g+=u.nodeValue.length);if(null===(p=u.firstChild))break;r=u;u=p}for(;;){if(u===a)break b;r===c&&++m===e&&(h=g);r===f&&++t===d&&(k=g);if(null!==(p=u.nextSibling))break;u=r;r=u.parentNode}u=p}c=-1===h||-1===k?null:
|
||||
{start:h,end:k}}else c=null}c=c||{start:0,end:0}}else c=null;Kf={focusedElem:a,selectionRange:c};Zc=!1;for(l=b;null!==l;)if(b=l,a=b.child,0!==(b.subtreeFlags&1028)&&null!==a)a.return=b,l=a;else for(;null!==l;){b=l;try{var x=b.alternate;if(0!==(b.flags&1024))switch(b.tag){case 0:case 11:case 15:break;case 1:if(null!==x){var w=x.memoizedProps,z=x.memoizedState,A=b.stateNode,v=A.getSnapshotBeforeUpdate(b.elementType===b.type?w:xa(b.type,w),z);A.__reactInternalSnapshotBeforeUpdate=v}break;case 3:var q=
|
||||
b.stateNode.containerInfo;1===q.nodeType?q.textContent="":9===q.nodeType&&q.documentElement&&q.removeChild(q.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(n(163));}}catch(M){H(b,b.return,M)}a=b.sibling;if(null!==a){a.return=b.return;l=a;break}l=b.return}x=Ai;Ai=!1;return x}function Gc(a,b,c){var d=b.updateQueue;d=null!==d?d.lastEffect:null;if(null!==d){var e=d=d.next;do{if((e.tag&a)===a){var f=e.destroy;e.destroy=void 0;void 0!==f&&If(b,c,f)}e=e.next}while(e!==d)}}
|
||||
function Id(a,b){b=b.updateQueue;b=null!==b?b.lastEffect:null;if(null!==b){var c=b=b.next;do{if((c.tag&a)===a){var d=c.create;c.destroy=d()}c=c.next}while(c!==b)}}function Lf(a){var b=a.ref;if(null!==b){var c=a.stateNode;switch(a.tag){case 5:a=c;break;default:a=c}"function"===typeof b?b(a):b.current=a}}function Bi(a){var b=a.alternate;null!==b&&(a.alternate=null,Bi(b));a.child=null;a.deletions=null;a.sibling=null;5===a.tag&&(b=a.stateNode,null!==b&&(delete b[Da],delete b[uc],delete b[Me],delete b[Ek],
|
||||
delete b[Fk]));a.stateNode=null;a.return=null;a.dependencies=null;a.memoizedProps=null;a.memoizedState=null;a.pendingProps=null;a.stateNode=null;a.updateQueue=null}function Ci(a){return 5===a.tag||3===a.tag||4===a.tag}function Di(a){a:for(;;){for(;null===a.sibling;){if(null===a.return||Ci(a.return))return null;a=a.return}a.sibling.return=a.return;for(a=a.sibling;5!==a.tag&&6!==a.tag&&18!==a.tag;){if(a.flags&2)continue a;if(null===a.child||4===a.tag)continue a;else a.child.return=a,a=a.child}if(!(a.flags&
|
||||
2))return a.stateNode}}function Mf(a,b,c){var d=a.tag;if(5===d||6===d)a=a.stateNode,b?8===c.nodeType?c.parentNode.insertBefore(a,b):c.insertBefore(a,b):(8===c.nodeType?(b=c.parentNode,b.insertBefore(a,c)):(b=c,b.appendChild(a)),c=c._reactRootContainer,null!==c&&void 0!==c||null!==b.onclick||(b.onclick=kd));else if(4!==d&&(a=a.child,null!==a))for(Mf(a,b,c),a=a.sibling;null!==a;)Mf(a,b,c),a=a.sibling}function Nf(a,b,c){var d=a.tag;if(5===d||6===d)a=a.stateNode,b?c.insertBefore(a,b):c.appendChild(a);
|
||||
else if(4!==d&&(a=a.child,null!==a))for(Nf(a,b,c),a=a.sibling;null!==a;)Nf(a,b,c),a=a.sibling}function jb(a,b,c){for(c=c.child;null!==c;)Ei(a,b,c),c=c.sibling}function Ei(a,b,c){if(Ca&&"function"===typeof Ca.onCommitFiberUnmount)try{Ca.onCommitFiberUnmount(Uc,c)}catch(h){}switch(c.tag){case 5:X||Wb(c,b);case 6:var d=T,e=za;T=null;jb(a,b,c);T=d;za=e;null!==T&&(za?(a=T,c=c.stateNode,8===a.nodeType?a.parentNode.removeChild(c):a.removeChild(c)):T.removeChild(c.stateNode));break;case 18:null!==T&&(za?
|
||||
(a=T,c=c.stateNode,8===a.nodeType?Re(a.parentNode,c):1===a.nodeType&&Re(a,c),nc(a)):Re(T,c.stateNode));break;case 4:d=T;e=za;T=c.stateNode.containerInfo;za=!0;jb(a,b,c);T=d;za=e;break;case 0:case 11:case 14:case 15:if(!X&&(d=c.updateQueue,null!==d&&(d=d.lastEffect,null!==d))){e=d=d.next;do{var f=e,g=f.destroy;f=f.tag;void 0!==g&&(0!==(f&2)?If(c,b,g):0!==(f&4)&&If(c,b,g));e=e.next}while(e!==d)}jb(a,b,c);break;case 1:if(!X&&(Wb(c,b),d=c.stateNode,"function"===typeof d.componentWillUnmount))try{d.props=
|
||||
c.memoizedProps,d.state=c.memoizedState,d.componentWillUnmount()}catch(h){H(c,b,h)}jb(a,b,c);break;case 21:jb(a,b,c);break;case 22:c.mode&1?(X=(d=X)||null!==c.memoizedState,jb(a,b,c),X=d):jb(a,b,c);break;default:jb(a,b,c)}}function Fi(a){var b=a.updateQueue;if(null!==b){a.updateQueue=null;var c=a.stateNode;null===c&&(c=a.stateNode=new Gk);b.forEach(function(b){var d=Hk.bind(null,a,b);c.has(b)||(c.add(b),b.then(d,d))})}}function Aa(a,b,c){c=b.deletions;if(null!==c)for(var d=0;d<c.length;d++){var e=
|
||||
c[d];try{var f=a,g=b,h=g;a:for(;null!==h;){switch(h.tag){case 5:T=h.stateNode;za=!1;break a;case 3:T=h.stateNode.containerInfo;za=!0;break a;case 4:T=h.stateNode.containerInfo;za=!0;break a}h=h.return}if(null===T)throw Error(n(160));Ei(f,g,e);T=null;za=!1;var k=e.alternate;null!==k&&(k.return=null);e.return=null}catch(m){H(e,b,m)}}if(b.subtreeFlags&12854)for(b=b.child;null!==b;)Gi(b,a),b=b.sibling}function Gi(a,b,c){var d=a.alternate;c=a.flags;switch(a.tag){case 0:case 11:case 14:case 15:Aa(b,a);
|
||||
Ha(a);if(c&4){try{Gc(3,a,a.return),Id(3,a)}catch(F){H(a,a.return,F)}try{Gc(5,a,a.return)}catch(F){H(a,a.return,F)}}break;case 1:Aa(b,a);Ha(a);c&512&&null!==d&&Wb(d,d.return);break;case 5:Aa(b,a);Ha(a);c&512&&null!==d&&Wb(d,d.return);if(a.flags&32){var e=a.stateNode;try{Fc(e,"")}catch(F){H(a,a.return,F)}}if(c&4&&(e=a.stateNode,null!=e)){var f=a.memoizedProps,g=null!==d?d.memoizedProps:f,h=a.type,k=a.updateQueue;a.updateQueue=null;if(null!==k)try{"input"===h&&"radio"===f.type&&null!=f.name&&lg(e,f);
|
||||
qe(h,g);var m=qe(h,f);for(g=0;g<k.length;g+=2){var t=k[g],u=k[g+1];"style"===t?sg(e,u):"dangerouslySetInnerHTML"===t?zi(e,u):"children"===t?Fc(e,u):$d(e,t,u,m)}switch(h){case "input":le(e,f);break;case "textarea":og(e,f);break;case "select":var r=e._wrapperState.wasMultiple;e._wrapperState.wasMultiple=!!f.multiple;var p=f.value;null!=p?Db(e,!!f.multiple,p,!1):r!==!!f.multiple&&(null!=f.defaultValue?Db(e,!!f.multiple,f.defaultValue,!0):Db(e,!!f.multiple,f.multiple?[]:"",!1))}e[uc]=f}catch(F){H(a,a.return,
|
||||
F)}}break;case 6:Aa(b,a);Ha(a);if(c&4){if(null===a.stateNode)throw Error(n(162));e=a.stateNode;f=a.memoizedProps;try{e.nodeValue=f}catch(F){H(a,a.return,F)}}break;case 3:Aa(b,a);Ha(a);if(c&4&&null!==d&&d.memoizedState.isDehydrated)try{nc(b.containerInfo)}catch(F){H(a,a.return,F)}break;case 4:Aa(b,a);Ha(a);break;case 13:Aa(b,a);Ha(a);e=a.child;e.flags&8192&&(f=null!==e.memoizedState,e.stateNode.isHidden=f,!f||null!==e.alternate&&null!==e.alternate.memoizedState||(Of=P()));c&4&&Fi(a);break;case 22:t=
|
||||
null!==d&&null!==d.memoizedState;a.mode&1?(X=(m=X)||t,Aa(b,a),X=m):Aa(b,a);Ha(a);if(c&8192){m=null!==a.memoizedState;if((a.stateNode.isHidden=m)&&!t&&0!==(a.mode&1))for(l=a,t=a.child;null!==t;){for(u=l=t;null!==l;){r=l;p=r.child;switch(r.tag){case 0:case 11:case 14:case 15:Gc(4,r,r.return);break;case 1:Wb(r,r.return);var x=r.stateNode;if("function"===typeof x.componentWillUnmount){c=r;b=r.return;try{d=c,x.props=d.memoizedProps,x.state=d.memoizedState,x.componentWillUnmount()}catch(F){H(c,b,F)}}break;
|
||||
case 5:Wb(r,r.return);break;case 22:if(null!==r.memoizedState){Hi(u);continue}}null!==p?(p.return=r,l=p):Hi(u)}t=t.sibling}a:for(t=null,u=a;;){if(5===u.tag){if(null===t){t=u;try{e=u.stateNode,m?(f=e.style,"function"===typeof f.setProperty?f.setProperty("display","none","important"):f.display="none"):(h=u.stateNode,k=u.memoizedProps.style,g=void 0!==k&&null!==k&&k.hasOwnProperty("display")?k.display:null,h.style.display=rg("display",g))}catch(F){H(a,a.return,F)}}}else if(6===u.tag){if(null===t)try{u.stateNode.nodeValue=
|
||||
m?"":u.memoizedProps}catch(F){H(a,a.return,F)}}else if((22!==u.tag&&23!==u.tag||null===u.memoizedState||u===a)&&null!==u.child){u.child.return=u;u=u.child;continue}if(u===a)break a;for(;null===u.sibling;){if(null===u.return||u.return===a)break a;t===u&&(t=null);u=u.return}t===u&&(t=null);u.sibling.return=u.return;u=u.sibling}}break;case 19:Aa(b,a);Ha(a);c&4&&Fi(a);break;case 21:break;default:Aa(b,a),Ha(a)}}function Ha(a){var b=a.flags;if(b&2){try{a:{for(var c=a.return;null!==c;){if(Ci(c)){var d=c;
|
||||
break a}c=c.return}throw Error(n(160));}switch(d.tag){case 5:var e=d.stateNode;d.flags&32&&(Fc(e,""),d.flags&=-33);var f=Di(a);Nf(a,f,e);break;case 3:case 4:var g=d.stateNode.containerInfo,h=Di(a);Mf(a,h,g);break;default:throw Error(n(161));}}catch(k){H(a,a.return,k)}a.flags&=-3}b&4096&&(a.flags&=-4097)}function Ik(a,b,c){l=a;Ii(a,b,c)}function Ii(a,b,c){for(var d=0!==(a.mode&1);null!==l;){var e=l,f=e.child;if(22===e.tag&&d){var g=null!==e.memoizedState||Jd;if(!g){var h=e.alternate,k=null!==h&&null!==
|
||||
h.memoizedState||X;h=Jd;var m=X;Jd=g;if((X=k)&&!m)for(l=e;null!==l;)g=l,k=g.child,22===g.tag&&null!==g.memoizedState?Ji(e):null!==k?(k.return=g,l=k):Ji(e);for(;null!==f;)l=f,Ii(f,b,c),f=f.sibling;l=e;Jd=h;X=m}Ki(a,b,c)}else 0!==(e.subtreeFlags&8772)&&null!==f?(f.return=e,l=f):Ki(a,b,c)}}function Ki(a,b,c){for(;null!==l;){b=l;if(0!==(b.flags&8772)){c=b.alternate;try{if(0!==(b.flags&8772))switch(b.tag){case 0:case 11:case 15:X||Id(5,b);break;case 1:var d=b.stateNode;if(b.flags&4&&!X)if(null===c)d.componentDidMount();
|
||||
else{var e=b.elementType===b.type?c.memoizedProps:xa(b.type,c.memoizedProps);d.componentDidUpdate(e,c.memoizedState,d.__reactInternalSnapshotBeforeUpdate)}var f=b.updateQueue;null!==f&&Fh(b,f,d);break;case 3:var g=b.updateQueue;if(null!==g){c=null;if(null!==b.child)switch(b.child.tag){case 5:c=b.child.stateNode;break;case 1:c=b.child.stateNode}Fh(b,g,c)}break;case 5:var h=b.stateNode;if(null===c&&b.flags&4){c=h;var k=b.memoizedProps;switch(b.type){case "button":case "input":case "select":case "textarea":k.autoFocus&&
|
||||
c.focus();break;case "img":k.src&&(c.src=k.src)}}break;case 6:break;case 4:break;case 12:break;case 13:if(null===b.memoizedState){var m=b.alternate;if(null!==m){var t=m.memoizedState;if(null!==t){var p=t.dehydrated;null!==p&&nc(p)}}}break;case 19:case 17:case 21:case 22:case 23:case 25:break;default:throw Error(n(163));}X||b.flags&512&&Lf(b)}catch(r){H(b,b.return,r)}}if(b===a){l=null;break}c=b.sibling;if(null!==c){c.return=b.return;l=c;break}l=b.return}}function Hi(a){for(;null!==l;){var b=l;if(b===
|
||||
a){l=null;break}var c=b.sibling;if(null!==c){c.return=b.return;l=c;break}l=b.return}}function Ji(a){for(;null!==l;){var b=l;try{switch(b.tag){case 0:case 11:case 15:var c=b.return;try{Id(4,b)}catch(k){H(b,c,k)}break;case 1:var d=b.stateNode;if("function"===typeof d.componentDidMount){var e=b.return;try{d.componentDidMount()}catch(k){H(b,e,k)}}var f=b.return;try{Lf(b)}catch(k){H(b,f,k)}break;case 5:var g=b.return;try{Lf(b)}catch(k){H(b,g,k)}}}catch(k){H(b,b.return,k)}if(b===a){l=null;break}var h=b.sibling;
|
||||
if(null!==h){h.return=b.return;l=h;break}l=b.return}}function Hc(){Hf=P()+500}function Z(){return 0!==(p&6)?P():-1!==Kd?Kd:Kd=P()}function hb(a){if(0===(a.mode&1))return 1;if(0!==(p&2)&&0!==U)return U&-U;if(null!==Jk.transition)return 0===Ld&&(Ld=Dg()),Ld;a=z;if(0!==a)return a;a=window.event;a=void 0===a?16:Lg(a.type);return a}function ya(a,b,c,d){if(50<Ic)throw Ic=0,Pf=null,Error(n(185));ic(a,c,d);if(0===(p&2)||a!==O)a===O&&(0===(p&2)&&(Md|=c),4===L&&kb(a,U)),ia(a,d),1===c&&0===p&&0===(b.mode&1)&&
|
||||
(Hc(),md&&db())}function ia(a,b){var c=a.callbackNode;uj(a,b);var d=Vc(a,a===O?U:0);if(0===d)null!==c&&Li(c),a.callbackNode=null,a.callbackPriority=0;else if(b=d&-d,a.callbackPriority!==b){null!=c&&Li(c);if(1===b)0===a.tag?kk(Mi.bind(null,a)):wh(Mi.bind(null,a)),Kk(function(){0===(p&6)&&db()}),c=null;else{switch(Eg(d)){case 1:c=De;break;case 4:c=Mg;break;case 16:c=ad;break;case 536870912:c=Ng;break;default:c=ad}c=Ni(c,Oi.bind(null,a))}a.callbackPriority=b;a.callbackNode=c}}function Oi(a,b){Kd=-1;
|
||||
Ld=0;if(0!==(p&6))throw Error(n(327));var c=a.callbackNode;if(Xb()&&a.callbackNode!==c)return null;var d=Vc(a,a===O?U:0);if(0===d)return null;if(0!==(d&30)||0!==(d&a.expiredLanes)||b)b=Nd(a,d);else{b=d;var e=p;p|=2;var f=Pi();if(O!==a||U!==b)Ra=null,Hc(),xb(a,b);do try{Lk();break}catch(h){Qi(a,h)}while(1);Ze();Od.current=f;p=e;null!==I?b=0:(O=null,U=0,b=L)}if(0!==b){2===b&&(e=ve(a),0!==e&&(d=e,b=Qf(a,e)));if(1===b)throw c=Jc,xb(a,0),kb(a,d),ia(a,P()),c;if(6===b)kb(a,d);else{e=a.current.alternate;
|
||||
if(0===(d&30)&&!Mk(e)&&(b=Nd(a,d),2===b&&(f=ve(a),0!==f&&(d=f,b=Qf(a,f))),1===b))throw c=Jc,xb(a,0),kb(a,d),ia(a,P()),c;a.finishedWork=e;a.finishedLanes=d;switch(b){case 0:case 1:throw Error(n(345));case 2:yb(a,ja,Ra);break;case 3:kb(a,d);if((d&130023424)===d&&(b=Of+500-P(),10<b)){if(0!==Vc(a,0))break;e=a.suspendedLanes;if((e&d)!==d){Z();a.pingedLanes|=a.suspendedLanes&e;break}a.timeoutHandle=Rf(yb.bind(null,a,ja,Ra),b);break}yb(a,ja,Ra);break;case 4:kb(a,d);if((d&4194240)===d)break;b=a.eventTimes;
|
||||
for(e=-1;0<d;){var g=31-ta(d);f=1<<g;g=b[g];g>e&&(e=g);d&=~f}d=e;d=P()-d;d=(120>d?120:480>d?480:1080>d?1080:1920>d?1920:3E3>d?3E3:4320>d?4320:1960*Nk(d/1960))-d;if(10<d){a.timeoutHandle=Rf(yb.bind(null,a,ja,Ra),d);break}yb(a,ja,Ra);break;case 5:yb(a,ja,Ra);break;default:throw Error(n(329));}}}ia(a,P());return a.callbackNode===c?Oi.bind(null,a):null}function Qf(a,b){var c=Kc;a.current.memoizedState.isDehydrated&&(xb(a,b).flags|=256);a=Nd(a,b);2!==a&&(b=ja,ja=c,null!==b&&Gf(b));return a}function Gf(a){null===
|
||||
ja?ja=a:ja.push.apply(ja,a)}function Mk(a){for(var b=a;;){if(b.flags&16384){var c=b.updateQueue;if(null!==c&&(c=c.stores,null!==c))for(var d=0;d<c.length;d++){var e=c[d],f=e.getSnapshot;e=e.value;try{if(!ua(f(),e))return!1}catch(g){return!1}}}c=b.child;if(b.subtreeFlags&16384&&null!==c)c.return=b,b=c;else{if(b===a)break;for(;null===b.sibling;){if(null===b.return||b.return===a)return!0;b=b.return}b.sibling.return=b.return;b=b.sibling}}return!0}function kb(a,b){b&=~Sf;b&=~Md;a.suspendedLanes|=b;a.pingedLanes&=
|
||||
~b;for(a=a.expirationTimes;0<b;){var c=31-ta(b),d=1<<c;a[c]=-1;b&=~d}}function Mi(a){if(0!==(p&6))throw Error(n(327));Xb();var b=Vc(a,0);if(0===(b&1))return ia(a,P()),null;var c=Nd(a,b);if(0!==a.tag&&2===c){var d=ve(a);0!==d&&(b=d,c=Qf(a,d))}if(1===c)throw c=Jc,xb(a,0),kb(a,b),ia(a,P()),c;if(6===c)throw Error(n(345));a.finishedWork=a.current.alternate;a.finishedLanes=b;yb(a,ja,Ra);ia(a,P());return null}function Tf(a,b){var c=p;p|=1;try{return a(b)}finally{p=c,0===p&&(Hc(),md&&db())}}function zb(a){null!==
|
||||
lb&&0===lb.tag&&0===(p&6)&&Xb();var b=p;p|=1;var c=ca.transition,d=z;try{if(ca.transition=null,z=1,a)return a()}finally{z=d,ca.transition=c,p=b,0===(p&6)&&db()}}function xb(a,b){a.finishedWork=null;a.finishedLanes=0;var c=a.timeoutHandle;-1!==c&&(a.timeoutHandle=-1,Ok(c));if(null!==I)for(c=I.return;null!==c;){var d=c;Ve(d);switch(d.tag){case 1:d=d.type.childContextTypes;null!==d&&void 0!==d&&(w(S),w(J));break;case 3:Tb();w(S);w(J);lf();break;case 5:kf(d);break;case 4:Tb();break;case 13:w(G);break;
|
||||
case 19:w(G);break;case 10:af(d.type._context);break;case 22:case 23:ba=Ga.current,w(Ga)}c=c.return}O=a;I=a=gb(a.current,null);U=ba=b;L=0;Jc=null;Sf=Md=ra=0;ja=Kc=null;if(null!==tb){for(b=0;b<tb.length;b++)if(c=tb[b],d=c.interleaved,null!==d){c.interleaved=null;var e=d.next,f=c.pending;if(null!==f){var g=f.next;f.next=e;d.next=g}c.pending=d}tb=null}return a}function Qi(a,b){do{var c=I;try{Ze();zd.current=Ad;if(Bd){for(var d=C.memoizedState;null!==d;){var e=d.queue;null!==e&&(e.pending=null);d=d.next}Bd=
|
||||
!1}wb=0;N=K=C=null;zc=!1;Ac=0;Uf.current=null;if(null===c||null===c.return){L=1;Jc=b;I=null;break}a:{var f=a,g=c.return,h=c,k=b;b=U;h.flags|=32768;if(null!==k&&"object"===typeof k&&"function"===typeof k.then){var m=k,l=h,p=l.tag;if(0===(l.mode&1)&&(0===p||11===p||15===p)){var r=l.alternate;r?(l.updateQueue=r.updateQueue,l.memoizedState=r.memoizedState,l.lanes=r.lanes):(l.updateQueue=null,l.memoizedState=null)}var w=ki(g);if(null!==w){w.flags&=-257;li(w,g,h,f,b);w.mode&1&&ji(f,m,b);b=w;k=m;var x=b.updateQueue;
|
||||
if(null===x){var z=new Set;z.add(k);b.updateQueue=z}else x.add(k);break a}else{if(0===(b&1)){ji(f,m,b);Ef();break a}k=Error(n(426))}}else if(D&&h.mode&1){var y=ki(g);if(null!==y){0===(y.flags&65536)&&(y.flags|=256);li(y,g,h,f,b);Ye(Ub(k,h));break a}}f=k=Ub(k,h);4!==L&&(L=2);null===Kc?Kc=[f]:Kc.push(f);f=g;do{switch(f.tag){case 3:f.flags|=65536;b&=-b;f.lanes|=b;var A=hi(f,k,b);Eh(f,A);break a;case 1:h=k;var v=f.type,q=f.stateNode;if(0===(f.flags&128)&&("function"===typeof v.getDerivedStateFromError||
|
||||
null!==q&&"function"===typeof q.componentDidCatch&&(null===ib||!ib.has(q)))){f.flags|=65536;b&=-b;f.lanes|=b;var B=ii(f,h,b);Eh(f,B);break a}}f=f.return}while(null!==f)}Ri(c)}catch(ma){b=ma;I===c&&null!==c&&(I=c=c.return);continue}break}while(1)}function Pi(){var a=Od.current;Od.current=Ad;return null===a?Ad:a}function Ef(){if(0===L||3===L||2===L)L=4;null===O||0===(ra&268435455)&&0===(Md&268435455)||kb(O,U)}function Nd(a,b){var c=p;p|=2;var d=Pi();if(O!==a||U!==b)Ra=null,xb(a,b);do try{Pk();break}catch(e){Qi(a,
|
||||
e)}while(1);Ze();p=c;Od.current=d;if(null!==I)throw Error(n(261));O=null;U=0;return L}function Pk(){for(;null!==I;)Si(I)}function Lk(){for(;null!==I&&!Qk();)Si(I)}function Si(a){var b=Rk(a.alternate,a,ba);a.memoizedProps=a.pendingProps;null===b?Ri(a):I=b;Uf.current=null}function Ri(a){var b=a;do{var c=b.alternate;a=b.return;if(0===(b.flags&32768)){if(c=yk(c,b,ba),null!==c){I=c;return}}else{c=Ck(c,b);if(null!==c){c.flags&=32767;I=c;return}if(null!==a)a.flags|=32768,a.subtreeFlags=0,a.deletions=null;
|
||||
else{L=6;I=null;return}}b=b.sibling;if(null!==b){I=b;return}I=b=a}while(null!==b);0===L&&(L=5)}function yb(a,b,c){var d=z,e=ca.transition;try{ca.transition=null,z=1,Sk(a,b,c,d)}finally{ca.transition=e,z=d}return null}function Sk(a,b,c,d){do Xb();while(null!==lb);if(0!==(p&6))throw Error(n(327));c=a.finishedWork;var e=a.finishedLanes;if(null===c)return null;a.finishedWork=null;a.finishedLanes=0;if(c===a.current)throw Error(n(177));a.callbackNode=null;a.callbackPriority=0;var f=c.lanes|c.childLanes;
|
||||
vj(a,f);a===O&&(I=O=null,U=0);0===(c.subtreeFlags&2064)&&0===(c.flags&2064)||Pd||(Pd=!0,Ni(ad,function(){Xb();return null}));f=0!==(c.flags&15990);if(0!==(c.subtreeFlags&15990)||f){f=ca.transition;ca.transition=null;var g=z;z=1;var h=p;p|=4;Uf.current=null;Dk(a,c);Gi(c,a);Uj(Kf);Zc=!!Jf;Kf=Jf=null;a.current=c;Ik(c,a,e);Tk();p=h;z=g;ca.transition=f}else a.current=c;Pd&&(Pd=!1,lb=a,Qd=e);f=a.pendingLanes;0===f&&(ib=null);pj(c.stateNode,d);ia(a,P());if(null!==b)for(d=a.onRecoverableError,c=0;c<b.length;c++)e=
|
||||
b[c],d(e.value,{componentStack:e.stack,digest:e.digest});if(Ed)throw Ed=!1,a=xf,xf=null,a;0!==(Qd&1)&&0!==a.tag&&Xb();f=a.pendingLanes;0!==(f&1)?a===Pf?Ic++:(Ic=0,Pf=a):Ic=0;db();return null}function Xb(){if(null!==lb){var a=Eg(Qd),b=ca.transition,c=z;try{ca.transition=null;z=16>a?16:a;if(null===lb)var d=!1;else{a=lb;lb=null;Qd=0;if(0!==(p&6))throw Error(n(331));var e=p;p|=4;for(l=a.current;null!==l;){var f=l,g=f.child;if(0!==(l.flags&16)){var h=f.deletions;if(null!==h){for(var k=0;k<h.length;k++){var m=
|
||||
h[k];for(l=m;null!==l;){var t=l;switch(t.tag){case 0:case 11:case 15:Gc(8,t,f)}var u=t.child;if(null!==u)u.return=t,l=u;else for(;null!==l;){t=l;var r=t.sibling,w=t.return;Bi(t);if(t===m){l=null;break}if(null!==r){r.return=w;l=r;break}l=w}}}var x=f.alternate;if(null!==x){var y=x.child;if(null!==y){x.child=null;do{var C=y.sibling;y.sibling=null;y=C}while(null!==y)}}l=f}}if(0!==(f.subtreeFlags&2064)&&null!==g)g.return=f,l=g;else b:for(;null!==l;){f=l;if(0!==(f.flags&2048))switch(f.tag){case 0:case 11:case 15:Gc(9,
|
||||
f,f.return)}var A=f.sibling;if(null!==A){A.return=f.return;l=A;break b}l=f.return}}var v=a.current;for(l=v;null!==l;){g=l;var q=g.child;if(0!==(g.subtreeFlags&2064)&&null!==q)q.return=g,l=q;else b:for(g=v;null!==l;){h=l;if(0!==(h.flags&2048))try{switch(h.tag){case 0:case 11:case 15:Id(9,h)}}catch(ma){H(h,h.return,ma)}if(h===g){l=null;break b}var B=h.sibling;if(null!==B){B.return=h.return;l=B;break b}l=h.return}}p=e;db();if(Ca&&"function"===typeof Ca.onPostCommitFiberRoot)try{Ca.onPostCommitFiberRoot(Uc,
|
||||
a)}catch(ma){}d=!0}return d}finally{z=c,ca.transition=b}}return!1}function Ti(a,b,c){b=Ub(c,b);b=hi(a,b,1);a=eb(a,b,1);b=Z();null!==a&&(ic(a,1,b),ia(a,b))}function H(a,b,c){if(3===a.tag)Ti(a,a,c);else for(;null!==b;){if(3===b.tag){Ti(b,a,c);break}else if(1===b.tag){var d=b.stateNode;if("function"===typeof b.type.getDerivedStateFromError||"function"===typeof d.componentDidCatch&&(null===ib||!ib.has(d))){a=Ub(c,a);a=ii(b,a,1);b=eb(b,a,1);a=Z();null!==b&&(ic(b,1,a),ia(b,a));break}}b=b.return}}function tk(a,
|
||||
b,c){var d=a.pingCache;null!==d&&d.delete(b);b=Z();a.pingedLanes|=a.suspendedLanes&c;O===a&&(U&c)===c&&(4===L||3===L&&(U&130023424)===U&&500>P()-Of?xb(a,0):Sf|=c);ia(a,b)}function Ui(a,b){0===b&&(0===(a.mode&1)?b=1:(b=Rd,Rd<<=1,0===(Rd&130023424)&&(Rd=4194304)));var c=Z();a=Oa(a,b);null!==a&&(ic(a,b,c),ia(a,c))}function wk(a){var b=a.memoizedState,c=0;null!==b&&(c=b.retryLane);Ui(a,c)}function Hk(a,b){var c=0;switch(a.tag){case 13:var d=a.stateNode;var e=a.memoizedState;null!==e&&(c=e.retryLane);
|
||||
break;case 19:d=a.stateNode;break;default:throw Error(n(314));}null!==d&&d.delete(b);Ui(a,c)}function Ni(a,b){return xh(a,b)}function Uk(a,b,c,d){this.tag=a;this.key=c;this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null;this.index=0;this.ref=null;this.pendingProps=b;this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null;this.mode=d;this.subtreeFlags=this.flags=0;this.deletions=null;this.childLanes=this.lanes=0;this.alternate=null}function yf(a){a=
|
||||
a.prototype;return!(!a||!a.isReactComponent)}function Vk(a){if("function"===typeof a)return yf(a)?1:0;if(void 0!==a&&null!==a){a=a.$$typeof;if(a===ie)return 11;if(a===je)return 14}return 2}function gb(a,b){var c=a.alternate;null===c?(c=pa(a.tag,b,a.key,a.mode),c.elementType=a.elementType,c.type=a.type,c.stateNode=a.stateNode,c.alternate=a,a.alternate=c):(c.pendingProps=b,c.type=a.type,c.flags=0,c.subtreeFlags=0,c.deletions=null);c.flags=a.flags&14680064;c.childLanes=a.childLanes;c.lanes=a.lanes;c.child=
|
||||
a.child;c.memoizedProps=a.memoizedProps;c.memoizedState=a.memoizedState;c.updateQueue=a.updateQueue;b=a.dependencies;c.dependencies=null===b?null:{lanes:b.lanes,firstContext:b.firstContext};c.sibling=a.sibling;c.index=a.index;c.ref=a.ref;return c}function wd(a,b,c,d,e,f){var g=2;d=a;if("function"===typeof a)yf(a)&&(g=1);else if("string"===typeof a)g=5;else a:switch(a){case Bb:return ub(c.children,e,f,b);case fe:g=8;e|=8;break;case ee:return a=pa(12,c,b,e|2),a.elementType=ee,a.lanes=f,a;case ge:return a=
|
||||
pa(13,c,b,e),a.elementType=ge,a.lanes=f,a;case he:return a=pa(19,c,b,e),a.elementType=he,a.lanes=f,a;case Vi:return Gd(c,e,f,b);default:if("object"===typeof a&&null!==a)switch(a.$$typeof){case hg:g=10;break a;case gg:g=9;break a;case ie:g=11;break a;case je:g=14;break a;case Ta:g=16;d=null;break a}throw Error(n(130,null==a?a:typeof a,""));}b=pa(g,c,b,e);b.elementType=a;b.type=d;b.lanes=f;return b}function ub(a,b,c,d){a=pa(7,a,d,b);a.lanes=c;return a}function Gd(a,b,c,d){a=pa(22,a,d,b);a.elementType=
|
||||
Vi;a.lanes=c;a.stateNode={isHidden:!1};return a}function gf(a,b,c){a=pa(6,a,null,b);a.lanes=c;return a}function hf(a,b,c){b=pa(4,null!==a.children?a.children:[],a.key,b);b.lanes=c;b.stateNode={containerInfo:a.containerInfo,pendingChildren:null,implementation:a.implementation};return b}function Wk(a,b,c,d,e){this.tag=b;this.containerInfo=a;this.finishedWork=this.pingCache=this.current=this.pendingChildren=null;this.timeoutHandle=-1;this.callbackNode=this.pendingContext=this.context=null;this.callbackPriority=
|
||||
0;this.eventTimes=we(0);this.expirationTimes=we(-1);this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0;this.entanglements=we(0);this.identifierPrefix=d;this.onRecoverableError=e;this.mutableSourceEagerHydrationData=null}function Vf(a,b,c,d,e,f,g,h,k,m){a=new Wk(a,b,c,h,k);1===b?(b=1,!0===f&&(b|=8)):b=0;f=pa(3,null,null,b);a.current=f;f.stateNode=a;f.memoizedState={element:d,isDehydrated:c,cache:null,transitions:null,
|
||||
pendingSuspenseBoundaries:null};df(f);return a}function Xk(a,b,c){var d=3<arguments.length&&void 0!==arguments[3]?arguments[3]:null;return{$$typeof:Cb,key:null==d?null:""+d,children:a,containerInfo:b,implementation:c}}function Wi(a){if(!a)return cb;a=a._reactInternals;a:{if(ob(a)!==a||1!==a.tag)throw Error(n(170));var b=a;do{switch(b.tag){case 3:b=b.stateNode.context;break a;case 1:if(ea(b.type)){b=b.stateNode.__reactInternalMemoizedMergedChildContext;break a}}b=b.return}while(null!==b);throw Error(n(171));
|
||||
}if(1===a.tag){var c=a.type;if(ea(c))return uh(a,c,b)}return b}function Xi(a,b,c,d,e,f,g,h,k,m){a=Vf(c,d,!0,a,e,f,g,h,k);a.context=Wi(null);c=a.current;d=Z();e=hb(c);f=Pa(d,e);f.callback=void 0!==b&&null!==b?b:null;eb(c,f,e);a.current.lanes=e;ic(a,e,d);ia(a,d);return a}function Sd(a,b,c,d){var e=b.current,f=Z(),g=hb(e);c=Wi(c);null===b.context?b.context=c:b.pendingContext=c;b=Pa(f,g);b.payload={element:a};d=void 0===d?null:d;null!==d&&(b.callback=d);a=eb(e,b,g);null!==a&&(ya(a,e,g,f),sd(a,e,g));return g}
|
||||
function Td(a){a=a.current;if(!a.child)return null;switch(a.child.tag){case 5:return a.child.stateNode;default:return a.child.stateNode}}function Yi(a,b){a=a.memoizedState;if(null!==a&&null!==a.dehydrated){var c=a.retryLane;a.retryLane=0!==c&&c<b?c:b}}function Wf(a,b){Yi(a,b);(a=a.alternate)&&Yi(a,b)}function Yk(a){a=Bg(a);return null===a?null:a.stateNode}function Zk(a){return null}function Xf(a){this._internalRoot=a}function Ud(a){this._internalRoot=a}function Yf(a){return!(!a||1!==a.nodeType&&9!==
|
||||
a.nodeType&&11!==a.nodeType)}function Vd(a){return!(!a||1!==a.nodeType&&9!==a.nodeType&&11!==a.nodeType&&(8!==a.nodeType||" react-mount-point-unstable "!==a.nodeValue))}function Zi(){}function $k(a,b,c,d,e){if(e){if("function"===typeof d){var f=d;d=function(){var a=Td(g);f.call(a)}}var g=Xi(b,d,a,0,null,!1,!1,"",Zi);a._reactRootContainer=g;a[Ja]=g.current;sc(8===a.nodeType?a.parentNode:a);zb();return g}for(;e=a.lastChild;)a.removeChild(e);if("function"===typeof d){var h=d;d=function(){var a=Td(k);
|
||||
h.call(a)}}var k=Vf(a,0,!1,null,null,!1,!1,"",Zi);a._reactRootContainer=k;a[Ja]=k.current;sc(8===a.nodeType?a.parentNode:a);zb(function(){Sd(b,k,c,d)});return k}function Wd(a,b,c,d,e){var f=c._reactRootContainer;if(f){var g=f;if("function"===typeof e){var h=e;e=function(){var a=Td(g);h.call(a)}}Sd(b,g,a,e)}else g=$k(c,b,a,e,d);return Td(g)}var cg=new Set,$b={},Ia=!("undefined"===typeof window||"undefined"===typeof window.document||"undefined"===typeof window.document.createElement),Zd=Object.prototype.hasOwnProperty,
|
||||
dj=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,eg={},dg={},R={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(a){R[a]=
|
||||
new Y(a,0,!1,a,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(a){var b=a[0];R[b]=new Y(b,1,!1,a[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(a){R[a]=new Y(a,2,!1,a.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(a){R[a]=new Y(a,2,!1,a,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(a){R[a]=
|
||||
new Y(a,3,!1,a.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(a){R[a]=new Y(a,3,!0,a,null,!1,!1)});["capture","download"].forEach(function(a){R[a]=new Y(a,4,!1,a,null,!1,!1)});["cols","rows","size","span"].forEach(function(a){R[a]=new Y(a,6,!1,a,null,!1,!1)});["rowSpan","start"].forEach(function(a){R[a]=new Y(a,5,!1,a.toLowerCase(),null,!1,!1)});var Zf=/[\-:]([a-z])/g,$f=function(a){return a[1].toUpperCase()};"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(a){var b=
|
||||
a.replace(Zf,$f);R[b]=new Y(b,1,!1,a,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(a){var b=a.replace(Zf,$f);R[b]=new Y(b,1,!1,a,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(a){var b=a.replace(Zf,$f);R[b]=new Y(b,1,!1,a,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(a){R[a]=new Y(a,1,!1,a.toLowerCase(),null,!1,!1)});R.xlinkHref=new Y("xlinkHref",
|
||||
1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(a){R[a]=new Y(a,1,!1,a.toLowerCase(),null,!0,!0)});var Sa=mb.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,xd=Symbol.for("react.element"),Cb=Symbol.for("react.portal"),Bb=Symbol.for("react.fragment"),fe=Symbol.for("react.strict_mode"),ee=Symbol.for("react.profiler"),hg=Symbol.for("react.provider"),gg=Symbol.for("react.context"),ie=Symbol.for("react.forward_ref"),ge=Symbol.for("react.suspense"),
|
||||
he=Symbol.for("react.suspense_list"),je=Symbol.for("react.memo"),Ta=Symbol.for("react.lazy");Symbol.for("react.scope");Symbol.for("react.debug_trace_mode");var Vi=Symbol.for("react.offscreen");Symbol.for("react.legacy_hidden");Symbol.for("react.cache");Symbol.for("react.tracing_marker");var fg=Symbol.iterator,E=Object.assign,ae,ce=!1,cc=Array.isArray,Xd,zi=function(a){return"undefined"!==typeof MSApp&&MSApp.execUnsafeLocalFunction?function(b,c,d,e){MSApp.execUnsafeLocalFunction(function(){return a(b,
|
||||
c,d,e)})}:a}(function(a,b){if("http://www.w3.org/2000/svg"!==a.namespaceURI||"innerHTML"in a)a.innerHTML=b;else{Xd=Xd||document.createElement("div");Xd.innerHTML="<svg>"+b.valueOf().toString()+"</svg>";for(b=Xd.firstChild;a.firstChild;)a.removeChild(a.firstChild);for(;b.firstChild;)a.appendChild(b.firstChild)}}),Fc=function(a,b){if(b){var c=a.firstChild;if(c&&c===a.lastChild&&3===c.nodeType){c.nodeValue=b;return}}a.textContent=b},dc={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,
|
||||
borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,
|
||||
strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},al=["Webkit","ms","Moz","O"];Object.keys(dc).forEach(function(a){al.forEach(function(b){b=b+a.charAt(0).toUpperCase()+a.substring(1);dc[b]=dc[a]})});var jj=E({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0}),ze=null,se=null,Eb=null,Fb=null,xg=function(a,b){return a(b)},yg=function(){},te=!1,Oe=!1;if(Ia)try{var Lc={};Object.defineProperty(Lc,
|
||||
"passive",{get:function(){Oe=!0}});window.addEventListener("test",Lc,Lc);window.removeEventListener("test",Lc,Lc)}catch(a){Oe=!1}var lj=function(a,b,c,d,e,f,g,h,k){var m=Array.prototype.slice.call(arguments,3);try{b.apply(c,m)}catch(t){this.onError(t)}},gc=!1,Sc=null,Tc=!1,ue=null,mj={onError:function(a){gc=!0;Sc=a}},Ba=mb.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler,Jg=Ba.unstable_scheduleCallback,Kg=Ba.unstable_NormalPriority,xh=Jg,Li=Ba.unstable_cancelCallback,Qk=Ba.unstable_shouldYield,
|
||||
Tk=Ba.unstable_requestPaint,P=Ba.unstable_now,Ej=Ba.unstable_getCurrentPriorityLevel,De=Ba.unstable_ImmediatePriority,Mg=Ba.unstable_UserBlockingPriority,ad=Kg,Fj=Ba.unstable_LowPriority,Ng=Ba.unstable_IdlePriority,Uc=null,Ca=null,ta=Math.clz32?Math.clz32:qj,rj=Math.log,sj=Math.LN2,Wc=64,Rd=4194304,z=0,Ae=!1,Yc=[],Va=null,Wa=null,Xa=null,jc=new Map,kc=new Map,Ya=[],Cj="mousedown mouseup touchcancel touchend touchstart auxclick dblclick pointercancel pointerdown pointerup dragend dragstart drop compositionend compositionstart keydown keypress keyup input textInput copy cut paste click change contextmenu reset submit".split(" "),
|
||||
Gb=Sa.ReactCurrentBatchConfig,Zc=!0,$c=null,Za=null,Ee=null,bd=null,Yb={eventPhase:0,bubbles:0,cancelable:0,timeStamp:function(a){return a.timeStamp||Date.now()},defaultPrevented:0,isTrusted:0},He=ka(Yb),Mc=E({},Yb,{view:0,detail:0}),bk=ka(Mc),ag,bg,Nc,Yd=E({},Mc,{screenX:0,screenY:0,clientX:0,clientY:0,pageX:0,pageY:0,ctrlKey:0,shiftKey:0,altKey:0,metaKey:0,getModifierState:Fe,button:0,buttons:0,relatedTarget:function(a){return void 0===a.relatedTarget?a.fromElement===a.srcElement?a.toElement:a.fromElement:
|
||||
a.relatedTarget},movementX:function(a){if("movementX"in a)return a.movementX;a!==Nc&&(Nc&&"mousemove"===a.type?(ag=a.screenX-Nc.screenX,bg=a.screenY-Nc.screenY):bg=ag=0,Nc=a);return ag},movementY:function(a){return"movementY"in a?a.movementY:bg}}),ih=ka(Yd),bl=E({},Yd,{dataTransfer:0}),Xj=ka(bl),cl=E({},Mc,{relatedTarget:0}),Pe=ka(cl),dl=E({},Yb,{animationName:0,elapsedTime:0,pseudoElement:0}),Zj=ka(dl),el=E({},Yb,{clipboardData:function(a){return"clipboardData"in a?a.clipboardData:window.clipboardData}}),
|
||||
dk=ka(el),fl=E({},Yb,{data:0}),qh=ka(fl),gk=qh,gl={Esc:"Escape",Spacebar:" ",Left:"ArrowLeft",Up:"ArrowUp",Right:"ArrowRight",Down:"ArrowDown",Del:"Delete",Win:"OS",Menu:"ContextMenu",Apps:"ContextMenu",Scroll:"ScrollLock",MozPrintableKey:"Unidentified"},hl={8:"Backspace",9:"Tab",12:"Clear",13:"Enter",16:"Shift",17:"Control",18:"Alt",19:"Pause",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",45:"Insert",46:"Delete",
|
||||
112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"NumLock",145:"ScrollLock",224:"Meta"},Hj={Alt:"altKey",Control:"ctrlKey",Meta:"metaKey",Shift:"shiftKey"},il=E({},Mc,{key:function(a){if(a.key){var b=gl[a.key]||a.key;if("Unidentified"!==b)return b}return"keypress"===a.type?(a=cd(a),13===a?"Enter":String.fromCharCode(a)):"keydown"===a.type||"keyup"===a.type?hl[a.keyCode]||"Unidentified":""},code:0,location:0,ctrlKey:0,shiftKey:0,altKey:0,
|
||||
metaKey:0,repeat:0,locale:0,getModifierState:Fe,charCode:function(a){return"keypress"===a.type?cd(a):0},keyCode:function(a){return"keydown"===a.type||"keyup"===a.type?a.keyCode:0},which:function(a){return"keypress"===a.type?cd(a):"keydown"===a.type||"keyup"===a.type?a.keyCode:0}}),Wj=ka(il),jl=E({},Yd,{pointerId:0,width:0,height:0,pressure:0,tangentialPressure:0,tiltX:0,tiltY:0,twist:0,pointerType:0,isPrimary:0}),nh=ka(jl),kl=E({},Mc,{touches:0,targetTouches:0,changedTouches:0,altKey:0,metaKey:0,
|
||||
ctrlKey:0,shiftKey:0,getModifierState:Fe}),Yj=ka(kl),ll=E({},Yb,{propertyName:0,elapsedTime:0,pseudoElement:0}),ak=ka(ll),ml=E({},Yd,{deltaX:function(a){return"deltaX"in a?a.deltaX:"wheelDeltaX"in a?-a.wheelDeltaX:0},deltaY:function(a){return"deltaY"in a?a.deltaY:"wheelDeltaY"in a?-a.wheelDeltaY:"wheelDelta"in a?-a.wheelDelta:0},deltaZ:0,deltaMode:0}),ck=ka(ml),Ij=[9,13,27,32],Ge=Ia&&"CompositionEvent"in window,Oc=null;Ia&&"documentMode"in document&&(Oc=document.documentMode);var fk=Ia&&"TextEvent"in
|
||||
window&&!Oc,Ug=Ia&&(!Ge||Oc&&8<Oc&&11>=Oc),Tg=String.fromCharCode(32),Sg=!1,Hb=!1,Lj={color:!0,date:!0,datetime:!0,"datetime-local":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0},oc=null,pc=null,ph=!1;Ia&&(ph=Mj("input")&&(!document.documentMode||9<document.documentMode));var ua="function"===typeof Object.is?Object.is:Tj,ek=Ia&&"documentMode"in document&&11>=document.documentMode,Jb=null,Ke=null,rc=null,Je=!1,Kb={animationend:gd("Animation","AnimationEnd"),
|
||||
animationiteration:gd("Animation","AnimationIteration"),animationstart:gd("Animation","AnimationStart"),transitionend:gd("Transition","TransitionEnd")},Le={},eh={};Ia&&(eh=document.createElement("div").style,"AnimationEvent"in window||(delete Kb.animationend.animation,delete Kb.animationiteration.animation,delete Kb.animationstart.animation),"TransitionEvent"in window||delete Kb.transitionend.transition);var jh=hd("animationend"),kh=hd("animationiteration"),lh=hd("animationstart"),mh=hd("transitionend"),
|
||||
fh=new Map,$i="abort auxClick cancel canPlay canPlayThrough click close contextMenu copy cut drag dragEnd dragEnter dragExit dragLeave dragOver dragStart drop durationChange emptied encrypted ended error gotPointerCapture input invalid keyDown keyPress keyUp load loadedData loadedMetadata loadStart lostPointerCapture mouseDown mouseMove mouseOut mouseOver mouseUp paste pause play playing pointerCancel pointerDown pointerMove pointerOut pointerOver pointerUp progress rateChange reset resize seeked seeking stalled submit suspend timeUpdate touchCancel touchEnd touchStart volumeChange scroll toggle touchMove waiting wheel".split(" ");
|
||||
(function(){for(var a=0;a<$i.length;a++){var b=$i[a],c=b.toLowerCase();b=b[0].toUpperCase()+b.slice(1);$a(c,"on"+b)}$a(jh,"onAnimationEnd");$a(kh,"onAnimationIteration");$a(lh,"onAnimationStart");$a("dblclick","onDoubleClick");$a("focusin","onFocus");$a("focusout","onBlur");$a(mh,"onTransitionEnd")})();Ab("onMouseEnter",["mouseout","mouseover"]);Ab("onMouseLeave",["mouseout","mouseover"]);Ab("onPointerEnter",["pointerout","pointerover"]);Ab("onPointerLeave",["pointerout","pointerover"]);nb("onChange",
|
||||
"change click focusin focusout input keydown keyup selectionchange".split(" "));nb("onSelect","focusout contextmenu dragend focusin keydown keyup mousedown mouseup selectionchange".split(" "));nb("onBeforeInput",["compositionend","keypress","textInput","paste"]);nb("onCompositionEnd","compositionend focusout keydown keypress keyup mousedown".split(" "));nb("onCompositionStart","compositionstart focusout keydown keypress keyup mousedown".split(" "));nb("onCompositionUpdate","compositionupdate focusout keydown keypress keyup mousedown".split(" "));
|
||||
var Ec="abort canplay canplaythrough durationchange emptied encrypted ended error loadeddata loadedmetadata loadstart pause play playing progress ratechange resize seeked seeking stalled suspend timeupdate volumechange waiting".split(" "),Vj=new Set("cancel close invalid load scroll toggle".split(" ").concat(Ec)),id="_reactListening"+Math.random().toString(36).slice(2),hk=/\r\n?/g,ik=/\u0000|\uFFFD/g,Jf=null,Kf=null,Rf="function"===typeof setTimeout?setTimeout:void 0,Ok="function"===typeof clearTimeout?
|
||||
clearTimeout:void 0,aj="function"===typeof Promise?Promise:void 0,Kk="function"===typeof queueMicrotask?queueMicrotask:"undefined"!==typeof aj?function(a){return aj.resolve(null).then(a).catch(jk)}:Rf,Zb=Math.random().toString(36).slice(2),Da="__reactFiber$"+Zb,uc="__reactProps$"+Zb,Ja="__reactContainer$"+Zb,Me="__reactEvents$"+Zb,Ek="__reactListeners$"+Zb,Fk="__reactHandles$"+Zb,Se=[],Mb=-1,cb={},J=bb(cb),S=bb(!1),qb=cb,La=null,md=!1,Te=!1,Ob=[],Pb=0,od=null,nd=0,na=[],oa=0,sb=null,Ma=1,Na="",la=
|
||||
null,fa=null,D=!1,wa=null,Jk=Sa.ReactCurrentBatchConfig,rd=bb(null),qd=null,Rb=null,$e=null,tb=null,lk=Oa,fb=!1,Jh=(new mb.Component).refs,ud={isMounted:function(a){return(a=a._reactInternals)?ob(a)===a:!1},enqueueSetState:function(a,b,c){a=a._reactInternals;var d=Z(),e=hb(a),f=Pa(d,e);f.payload=b;void 0!==c&&null!==c&&(f.callback=c);b=eb(a,f,e);null!==b&&(ya(b,a,e,d),sd(b,a,e))},enqueueReplaceState:function(a,b,c){a=a._reactInternals;var d=Z(),e=hb(a),f=Pa(d,e);f.tag=1;f.payload=b;void 0!==c&&null!==
|
||||
c&&(f.callback=c);b=eb(a,f,e);null!==b&&(ya(b,a,e,d),sd(b,a,e))},enqueueForceUpdate:function(a,b){a=a._reactInternals;var c=Z(),d=hb(a),e=Pa(c,d);e.tag=2;void 0!==b&&null!==b&&(e.callback=b);b=eb(a,e,d);null!==b&&(ya(b,a,d,c),sd(b,a,d))}},Vb=Lh(!0),mi=Lh(!1),wc={},Ea=bb(wc),yc=bb(wc),xc=bb(wc),G=bb(0),mf=[],zd=Sa.ReactCurrentDispatcher,uf=Sa.ReactCurrentBatchConfig,wb=0,C=null,K=null,N=null,Bd=!1,zc=!1,Ac=0,nl=0,Ad={readContext:qa,useCallback:V,useContext:V,useEffect:V,useImperativeHandle:V,useInsertionEffect:V,
|
||||
useLayoutEffect:V,useMemo:V,useReducer:V,useRef:V,useState:V,useDebugValue:V,useDeferredValue:V,useTransition:V,useMutableSource:V,useSyncExternalStore:V,useId:V,unstable_isNewReconciler:!1},mk={readContext:qa,useCallback:function(a,b){Fa().memoizedState=[a,void 0===b?null:b];return a},useContext:qa,useEffect:Wh,useImperativeHandle:function(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;return Cd(4194308,4,Zh.bind(null,b,a),c)},useLayoutEffect:function(a,b){return Cd(4194308,4,a,b)},useInsertionEffect:function(a,
|
||||
b){return Cd(4,2,a,b)},useMemo:function(a,b){var c=Fa();b=void 0===b?null:b;a=a();c.memoizedState=[a,b];return a},useReducer:function(a,b,c){var d=Fa();b=void 0!==c?c(b):b;d.memoizedState=d.baseState=b;a={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:a,lastRenderedState:b};d.queue=a;a=a.dispatch=rk.bind(null,C,a);return[d.memoizedState,a]},useRef:function(a){var b=Fa();a={current:a};return b.memoizedState=a},useState:Uh,useDebugValue:tf,useDeferredValue:function(a){return Fa().memoizedState=
|
||||
a},useTransition:function(){var a=Uh(!1),b=a[0];a=qk.bind(null,a[1]);Fa().memoizedState=a;return[b,a]},useMutableSource:function(a,b,c){},useSyncExternalStore:function(a,b,c){var d=C,e=Fa();if(D){if(void 0===c)throw Error(n(407));c=c()}else{c=b();if(null===O)throw Error(n(349));0!==(wb&30)||Rh(d,b,c)}e.memoizedState=c;var f={value:c,getSnapshot:b};e.queue=f;Wh(Ph.bind(null,d,f,a),[a]);d.flags|=2048;Cc(9,Qh.bind(null,d,f,c,b),void 0,null);return c},useId:function(){var a=Fa(),b=O.identifierPrefix;
|
||||
if(D){var c=Na;var d=Ma;c=(d&~(1<<32-ta(d)-1)).toString(32)+c;b=":"+b+"R"+c;c=Ac++;0<c&&(b+="H"+c.toString(32));b+=":"}else c=nl++,b=":"+b+"r"+c.toString(32)+":";return a.memoizedState=b},unstable_isNewReconciler:!1},nk={readContext:qa,useCallback:ai,useContext:qa,useEffect:sf,useImperativeHandle:$h,useInsertionEffect:Xh,useLayoutEffect:Yh,useMemo:bi,useReducer:qf,useRef:Vh,useState:function(a){return qf(Bc)},useDebugValue:tf,useDeferredValue:function(a){var b=sa();return ci(b,K.memoizedState,a)},
|
||||
useTransition:function(){var a=qf(Bc)[0],b=sa().memoizedState;return[a,b]},useMutableSource:Nh,useSyncExternalStore:Oh,useId:di,unstable_isNewReconciler:!1},ok={readContext:qa,useCallback:ai,useContext:qa,useEffect:sf,useImperativeHandle:$h,useInsertionEffect:Xh,useLayoutEffect:Yh,useMemo:bi,useReducer:rf,useRef:Vh,useState:function(a){return rf(Bc)},useDebugValue:tf,useDeferredValue:function(a){var b=sa();return null===K?b.memoizedState=a:ci(b,K.memoizedState,a)},useTransition:function(){var a=rf(Bc)[0],
|
||||
b=sa().memoizedState;return[a,b]},useMutableSource:Nh,useSyncExternalStore:Oh,useId:di,unstable_isNewReconciler:!1},sk="function"===typeof WeakMap?WeakMap:Map,uk=Sa.ReactCurrentOwner,ha=!1,Cf={dehydrated:null,treeContext:null,retryLane:0};var Ak=function(a,b,c,d){for(c=b.child;null!==c;){if(5===c.tag||6===c.tag)a.appendChild(c.stateNode);else if(4!==c.tag&&null!==c.child){c.child.return=c;c=c.child;continue}if(c===b)break;for(;null===c.sibling;){if(null===c.return||c.return===b)return;c=c.return}c.sibling.return=
|
||||
c.return;c=c.sibling}};var yi=function(a,b){};var zk=function(a,b,c,d,e){var f=a.memoizedProps;if(f!==d){a=b.stateNode;vb(Ea.current);e=null;switch(c){case "input":f=ke(a,f);d=ke(a,d);e=[];break;case "select":f=E({},f,{value:void 0});d=E({},d,{value:void 0});e=[];break;case "textarea":f=ne(a,f);d=ne(a,d);e=[];break;default:"function"!==typeof f.onClick&&"function"===typeof d.onClick&&(a.onclick=kd)}pe(c,d);var g;c=null;for(m in f)if(!d.hasOwnProperty(m)&&f.hasOwnProperty(m)&&null!=f[m])if("style"===
|
||||
m){var h=f[m];for(g in h)h.hasOwnProperty(g)&&(c||(c={}),c[g]="")}else"dangerouslySetInnerHTML"!==m&&"children"!==m&&"suppressContentEditableWarning"!==m&&"suppressHydrationWarning"!==m&&"autoFocus"!==m&&($b.hasOwnProperty(m)?e||(e=[]):(e=e||[]).push(m,null));for(m in d){var k=d[m];h=null!=f?f[m]:void 0;if(d.hasOwnProperty(m)&&k!==h&&(null!=k||null!=h))if("style"===m)if(h){for(g in h)!h.hasOwnProperty(g)||k&&k.hasOwnProperty(g)||(c||(c={}),c[g]="");for(g in k)k.hasOwnProperty(g)&&h[g]!==k[g]&&(c||
|
||||
(c={}),c[g]=k[g])}else c||(e||(e=[]),e.push(m,c)),c=k;else"dangerouslySetInnerHTML"===m?(k=k?k.__html:void 0,h=h?h.__html:void 0,null!=k&&h!==k&&(e=e||[]).push(m,k)):"children"===m?"string"!==typeof k&&"number"!==typeof k||(e=e||[]).push(m,""+k):"suppressContentEditableWarning"!==m&&"suppressHydrationWarning"!==m&&($b.hasOwnProperty(m)?(null!=k&&"onScroll"===m&&B("scroll",a),e||h===k||(e=[])):(e=e||[]).push(m,k))}c&&(e=e||[]).push("style",c);var m=e;if(b.updateQueue=m)b.flags|=4}};var Bk=function(a,
|
||||
b,c,d){c!==d&&(b.flags|=4)};var Jd=!1,X=!1,Gk="function"===typeof WeakSet?WeakSet:Set,l=null,Ai=!1,T=null,za=!1,Nk=Math.ceil,Od=Sa.ReactCurrentDispatcher,Uf=Sa.ReactCurrentOwner,ca=Sa.ReactCurrentBatchConfig,p=0,O=null,I=null,U=0,ba=0,Ga=bb(0),L=0,Jc=null,ra=0,Md=0,Sf=0,Kc=null,ja=null,Of=0,Hf=Infinity,Ra=null,Ed=!1,xf=null,ib=null,Pd=!1,lb=null,Qd=0,Ic=0,Pf=null,Kd=-1,Ld=0;var Rk=function(a,b,c){if(null!==a)if(a.memoizedProps!==b.pendingProps||S.current)ha=!0;else{if(0===(a.lanes&c)&&0===(b.flags&
|
||||
128))return ha=!1,xk(a,b,c);ha=0!==(a.flags&131072)?!0:!1}else ha=!1,D&&0!==(b.flags&1048576)&&yh(b,nd,b.index);b.lanes=0;switch(b.tag){case 2:var d=b.type;Fd(a,b);a=b.pendingProps;var e=Nb(b,J.current);Sb(b,c);e=of(null,b,d,a,e,c);var f=pf();b.flags|=1;"object"===typeof e&&null!==e&&"function"===typeof e.render&&void 0===e.$$typeof?(b.tag=1,b.memoizedState=null,b.updateQueue=null,ea(d)?(f=!0,ld(b)):f=!1,b.memoizedState=null!==e.state&&void 0!==e.state?e.state:null,df(b),e.updater=ud,b.stateNode=
|
||||
e,e._reactInternals=b,ff(b,d,a,c),b=Af(null,b,d,!0,f,c)):(b.tag=0,D&&f&&Ue(b),aa(null,b,e,c),b=b.child);return b;case 16:d=b.elementType;a:{Fd(a,b);a=b.pendingProps;e=d._init;d=e(d._payload);b.type=d;e=b.tag=Vk(d);a=xa(d,a);switch(e){case 0:b=zf(null,b,d,a,c);break a;case 1:b=si(null,b,d,a,c);break a;case 11:b=ni(null,b,d,a,c);break a;case 14:b=oi(null,b,d,xa(d.type,a),c);break a}throw Error(n(306,d,""));}return b;case 0:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:xa(d,e),zf(a,b,d,e,c);
|
||||
case 1:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:xa(d,e),si(a,b,d,e,c);case 3:a:{ti(b);if(null===a)throw Error(n(387));d=b.pendingProps;f=b.memoizedState;e=f.element;Dh(a,b);td(b,d,null,c);var g=b.memoizedState;d=g.element;if(f.isDehydrated)if(f={element:d,isDehydrated:!1,cache:g.cache,pendingSuspenseBoundaries:g.pendingSuspenseBoundaries,transitions:g.transitions},b.updateQueue.baseState=f,b.memoizedState=f,b.flags&256){e=Ub(Error(n(423)),b);b=ui(a,b,d,c,e);break a}else if(d!==e){e=
|
||||
Ub(Error(n(424)),b);b=ui(a,b,d,c,e);break a}else for(fa=Ka(b.stateNode.containerInfo.firstChild),la=b,D=!0,wa=null,c=mi(b,null,d,c),b.child=c;c;)c.flags=c.flags&-3|4096,c=c.sibling;else{Qb();if(d===e){b=Qa(a,b,c);break a}aa(a,b,d,c)}b=b.child}return b;case 5:return Mh(b),null===a&&Xe(b),d=b.type,e=b.pendingProps,f=null!==a?a.memoizedProps:null,g=e.children,Qe(d,e)?g=null:null!==f&&Qe(d,f)&&(b.flags|=32),ri(a,b),aa(a,b,g,c),b.child;case 6:return null===a&&Xe(b),null;case 13:return vi(a,b,c);case 4:return jf(b,
|
||||
b.stateNode.containerInfo),d=b.pendingProps,null===a?b.child=Vb(b,null,d,c):aa(a,b,d,c),b.child;case 11:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:xa(d,e),ni(a,b,d,e,c);case 7:return aa(a,b,b.pendingProps,c),b.child;case 8:return aa(a,b,b.pendingProps.children,c),b.child;case 12:return aa(a,b,b.pendingProps.children,c),b.child;case 10:a:{d=b.type._context;e=b.pendingProps;f=b.memoizedProps;g=e.value;y(rd,d._currentValue);d._currentValue=g;if(null!==f)if(ua(f.value,g)){if(f.children===
|
||||
e.children&&!S.current){b=Qa(a,b,c);break a}}else for(f=b.child,null!==f&&(f.return=b);null!==f;){var h=f.dependencies;if(null!==h){g=f.child;for(var k=h.firstContext;null!==k;){if(k.context===d){if(1===f.tag){k=Pa(-1,c&-c);k.tag=2;var m=f.updateQueue;if(null!==m){m=m.shared;var l=m.pending;null===l?k.next=k:(k.next=l.next,l.next=k);m.pending=k}}f.lanes|=c;k=f.alternate;null!==k&&(k.lanes|=c);bf(f.return,c,b);h.lanes|=c;break}k=k.next}}else if(10===f.tag)g=f.type===b.type?null:f.child;else if(18===
|
||||
f.tag){g=f.return;if(null===g)throw Error(n(341));g.lanes|=c;h=g.alternate;null!==h&&(h.lanes|=c);bf(g,c,b);g=f.sibling}else g=f.child;if(null!==g)g.return=f;else for(g=f;null!==g;){if(g===b){g=null;break}f=g.sibling;if(null!==f){f.return=g.return;g=f;break}g=g.return}f=g}aa(a,b,e.children,c);b=b.child}return b;case 9:return e=b.type,d=b.pendingProps.children,Sb(b,c),e=qa(e),d=d(e),b.flags|=1,aa(a,b,d,c),b.child;case 14:return d=b.type,e=xa(d,b.pendingProps),e=xa(d.type,e),oi(a,b,d,e,c);case 15:return pi(a,
|
||||
b,b.type,b.pendingProps,c);case 17:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:xa(d,e),Fd(a,b),b.tag=1,ea(d)?(a=!0,ld(b)):a=!1,Sb(b,c),Hh(b,d,e),ff(b,d,e,c),Af(null,b,d,!0,a,c);case 19:return xi(a,b,c);case 22:return qi(a,b,c)}throw Error(n(156,b.tag));};var pa=function(a,b,c,d){return new Uk(a,b,c,d)},bj="function"===typeof reportError?reportError:function(a){console.error(a)};Ud.prototype.render=Xf.prototype.render=function(a){var b=this._internalRoot;if(null===b)throw Error(n(409));
|
||||
Sd(a,b,null,null)};Ud.prototype.unmount=Xf.prototype.unmount=function(){var a=this._internalRoot;if(null!==a){this._internalRoot=null;var b=a.containerInfo;zb(function(){Sd(null,a,null,null)});b[Ja]=null}};Ud.prototype.unstable_scheduleHydration=function(a){if(a){var b=ol();a={blockedOn:null,target:a,priority:b};for(var c=0;c<Ya.length&&0!==b&&b<Ya[c].priority;c++);Ya.splice(c,0,a);0===c&&Hg(a)}};var Dj=function(a){switch(a.tag){case 3:var b=a.stateNode;if(b.current.memoizedState.isDehydrated){var c=
|
||||
hc(b.pendingLanes);0!==c&&(xe(b,c|1),ia(b,P()),0===(p&6)&&(Hc(),db()))}break;case 13:zb(function(){var b=Oa(a,1);if(null!==b){var c=Z();ya(b,a,1,c)}}),Wf(a,1)}};var Gg=function(a){if(13===a.tag){var b=Oa(a,134217728);if(null!==b){var c=Z();ya(b,a,134217728,c)}Wf(a,134217728)}};var yj=function(a){if(13===a.tag){var b=hb(a),c=Oa(a,b);if(null!==c){var d=Z();ya(c,a,b,d)}Wf(a,b)}};var ol=function(){return z};var xj=function(a,b){var c=z;try{return z=a,b()}finally{z=c}};se=function(a,b,c){switch(b){case "input":le(a,
|
||||
c);b=c.name;if("radio"===c.type&&null!=b){for(c=a;c.parentNode;)c=c.parentNode;c=c.querySelectorAll("input[name="+JSON.stringify(""+b)+'][type="radio"]');for(b=0;b<c.length;b++){var d=c[b];if(d!==a&&d.form===a.form){var e=Rc(d);if(!e)throw Error(n(90));jg(d);le(d,e)}}}break;case "textarea":og(a,c);break;case "select":b=c.value,null!=b&&Db(a,!!c.multiple,b,!1)}};(function(a,b,c){xg=a;yg=c})(Tf,function(a,b,c,d,e){var f=z,g=ca.transition;try{return ca.transition=null,z=1,a(b,c,d,e)}finally{z=f,ca.transition=
|
||||
g,0===p&&Hc()}},zb);var pl={usingClientEntryPoint:!1,Events:[ec,Ib,Rc,ug,vg,Tf]};(function(a){a={bundleType:a.bundleType,version:a.version,rendererPackageName:a.rendererPackageName,rendererConfig:a.rendererConfig,overrideHookState:null,overrideHookStateDeletePath:null,overrideHookStateRenamePath:null,overrideProps:null,overridePropsDeletePath:null,overridePropsRenamePath:null,setErrorHandler:null,setSuspenseHandler:null,scheduleUpdate:null,currentDispatcherRef:Sa.ReactCurrentDispatcher,findHostInstanceByFiber:Yk,
|
||||
findFiberByHostInstance:a.findFiberByHostInstance||Zk,findHostInstancesForRefresh:null,scheduleRefresh:null,scheduleRoot:null,setRefreshHandler:null,getCurrentFiber:null,reconcilerVersion:"18.2.0"};if("undefined"===typeof __REACT_DEVTOOLS_GLOBAL_HOOK__)a=!1;else{var b=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(b.isDisabled||!b.supportsFiber)a=!0;else{try{Uc=b.inject(a),Ca=b}catch(c){}a=b.checkDCE?!0:!1}}return a})({findFiberByHostInstance:pb,bundleType:0,version:"18.2.0-next-9e3b772b8-20220608",
|
||||
rendererPackageName:"react-dom"});Q.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=pl;Q.createPortal=function(a,b){var c=2<arguments.length&&void 0!==arguments[2]?arguments[2]:null;if(!Yf(b))throw Error(n(200));return Xk(a,b,null,c)};Q.createRoot=function(a,b){if(!Yf(a))throw Error(n(299));var c=!1,d="",e=bj;null!==b&&void 0!==b&&(!0===b.unstable_strictMode&&(c=!0),void 0!==b.identifierPrefix&&(d=b.identifierPrefix),void 0!==b.onRecoverableError&&(e=b.onRecoverableError));b=Vf(a,1,!1,null,null,
|
||||
c,!1,d,e);a[Ja]=b.current;sc(8===a.nodeType?a.parentNode:a);return new Xf(b)};Q.findDOMNode=function(a){if(null==a)return null;if(1===a.nodeType)return a;var b=a._reactInternals;if(void 0===b){if("function"===typeof a.render)throw Error(n(188));a=Object.keys(a).join(",");throw Error(n(268,a));}a=Bg(b);a=null===a?null:a.stateNode;return a};Q.flushSync=function(a){return zb(a)};Q.hydrate=function(a,b,c){if(!Vd(b))throw Error(n(200));return Wd(null,a,b,!0,c)};Q.hydrateRoot=function(a,b,c){if(!Yf(a))throw Error(n(405));
|
||||
var d=null!=c&&c.hydratedSources||null,e=!1,f="",g=bj;null!==c&&void 0!==c&&(!0===c.unstable_strictMode&&(e=!0),void 0!==c.identifierPrefix&&(f=c.identifierPrefix),void 0!==c.onRecoverableError&&(g=c.onRecoverableError));b=Xi(b,null,a,1,null!=c?c:null,e,!1,f,g);a[Ja]=b.current;sc(a);if(d)for(a=0;a<d.length;a++)c=d[a],e=c._getVersion,e=e(c._source),null==b.mutableSourceEagerHydrationData?b.mutableSourceEagerHydrationData=[c,e]:b.mutableSourceEagerHydrationData.push(c,e);return new Ud(b)};Q.render=
|
||||
function(a,b,c){if(!Vd(b))throw Error(n(200));return Wd(null,a,b,!1,c)};Q.unmountComponentAtNode=function(a){if(!Vd(a))throw Error(n(40));return a._reactRootContainer?(zb(function(){Wd(null,null,a,!1,function(){a._reactRootContainer=null;a[Ja]=null})}),!0):!1};Q.unstable_batchedUpdates=Tf;Q.unstable_renderSubtreeIntoContainer=function(a,b,c,d){if(!Vd(c))throw Error(n(200));if(null==a||void 0===a._reactInternals)throw Error(n(38));return Wd(a,b,c,!1,d)};Q.version="18.2.0-next-9e3b772b8-20220608"});
|
||||
})();
|
||||
+3343
File diff suppressed because it is too large
Load Diff
+31
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @license React
|
||||
* react.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
(function(){'use strict';(function(c,x){"object"===typeof exports&&"undefined"!==typeof module?x(exports):"function"===typeof define&&define.amd?define(["exports"],x):(c=c||self,x(c.React={}))})(this,function(c){function x(a){if(null===a||"object"!==typeof a)return null;a=V&&a[V]||a["@@iterator"];return"function"===typeof a?a:null}function w(a,b,e){this.props=a;this.context=b;this.refs=W;this.updater=e||X}function Y(){}function K(a,b,e){this.props=a;this.context=b;this.refs=W;this.updater=e||X}function Z(a,b,
|
||||
e){var m,d={},c=null,h=null;if(null!=b)for(m in void 0!==b.ref&&(h=b.ref),void 0!==b.key&&(c=""+b.key),b)aa.call(b,m)&&!ba.hasOwnProperty(m)&&(d[m]=b[m]);var l=arguments.length-2;if(1===l)d.children=e;else if(1<l){for(var f=Array(l),k=0;k<l;k++)f[k]=arguments[k+2];d.children=f}if(a&&a.defaultProps)for(m in l=a.defaultProps,l)void 0===d[m]&&(d[m]=l[m]);return{$$typeof:y,type:a,key:c,ref:h,props:d,_owner:L.current}}function na(a,b){return{$$typeof:y,type:a.type,key:b,ref:a.ref,props:a.props,_owner:a._owner}}
|
||||
function M(a){return"object"===typeof a&&null!==a&&a.$$typeof===y}function oa(a){var b={"=":"=0",":":"=2"};return"$"+a.replace(/[=:]/g,function(a){return b[a]})}function N(a,b){return"object"===typeof a&&null!==a&&null!=a.key?oa(""+a.key):b.toString(36)}function B(a,b,e,m,d){var c=typeof a;if("undefined"===c||"boolean"===c)a=null;var h=!1;if(null===a)h=!0;else switch(c){case "string":case "number":h=!0;break;case "object":switch(a.$$typeof){case y:case pa:h=!0}}if(h)return h=a,d=d(h),a=""===m?"."+
|
||||
N(h,0):m,ca(d)?(e="",null!=a&&(e=a.replace(da,"$&/")+"/"),B(d,b,e,"",function(a){return a})):null!=d&&(M(d)&&(d=na(d,e+(!d.key||h&&h.key===d.key?"":(""+d.key).replace(da,"$&/")+"/")+a)),b.push(d)),1;h=0;m=""===m?".":m+":";if(ca(a))for(var l=0;l<a.length;l++){c=a[l];var f=m+N(c,l);h+=B(c,b,e,f,d)}else if(f=x(a),"function"===typeof f)for(a=f.call(a),l=0;!(c=a.next()).done;)c=c.value,f=m+N(c,l++),h+=B(c,b,e,f,d);else if("object"===c)throw b=String(a),Error("Objects are not valid as a React child (found: "+
|
||||
("[object Object]"===b?"object with keys {"+Object.keys(a).join(", ")+"}":b)+"). If you meant to render a collection of children, use an array instead.");return h}function C(a,b,e){if(null==a)return a;var c=[],d=0;B(a,c,"","",function(a){return b.call(e,a,d++)});return c}function qa(a){if(-1===a._status){var b=a._result;b=b();b.then(function(b){if(0===a._status||-1===a._status)a._status=1,a._result=b},function(b){if(0===a._status||-1===a._status)a._status=2,a._result=b});-1===a._status&&(a._status=
|
||||
0,a._result=b)}if(1===a._status)return a._result.default;throw a._result;}function O(a,b){var e=a.length;a.push(b);a:for(;0<e;){var c=e-1>>>1,d=a[c];if(0<D(d,b))a[c]=b,a[e]=d,e=c;else break a}}function p(a){return 0===a.length?null:a[0]}function E(a){if(0===a.length)return null;var b=a[0],e=a.pop();if(e!==b){a[0]=e;a:for(var c=0,d=a.length,k=d>>>1;c<k;){var h=2*(c+1)-1,l=a[h],f=h+1,g=a[f];if(0>D(l,e))f<d&&0>D(g,l)?(a[c]=g,a[f]=e,c=f):(a[c]=l,a[h]=e,c=h);else if(f<d&&0>D(g,e))a[c]=g,a[f]=e,c=f;else break a}}return b}
|
||||
function D(a,b){var c=a.sortIndex-b.sortIndex;return 0!==c?c:a.id-b.id}function P(a){for(var b=p(r);null!==b;){if(null===b.callback)E(r);else if(b.startTime<=a)E(r),b.sortIndex=b.expirationTime,O(q,b);else break;b=p(r)}}function Q(a){z=!1;P(a);if(!u)if(null!==p(q))u=!0,R(S);else{var b=p(r);null!==b&&T(Q,b.startTime-a)}}function S(a,b){u=!1;z&&(z=!1,ea(A),A=-1);F=!0;var c=k;try{P(b);for(n=p(q);null!==n&&(!(n.expirationTime>b)||a&&!fa());){var m=n.callback;if("function"===typeof m){n.callback=null;
|
||||
k=n.priorityLevel;var d=m(n.expirationTime<=b);b=v();"function"===typeof d?n.callback=d:n===p(q)&&E(q);P(b)}else E(q);n=p(q)}if(null!==n)var g=!0;else{var h=p(r);null!==h&&T(Q,h.startTime-b);g=!1}return g}finally{n=null,k=c,F=!1}}function fa(){return v()-ha<ia?!1:!0}function R(a){G=a;H||(H=!0,I())}function T(a,b){A=ja(function(){a(v())},b)}var y=Symbol.for("react.element"),pa=Symbol.for("react.portal"),ra=Symbol.for("react.fragment"),sa=Symbol.for("react.strict_mode"),ta=Symbol.for("react.profiler"),
|
||||
ua=Symbol.for("react.provider"),va=Symbol.for("react.context"),wa=Symbol.for("react.forward_ref"),xa=Symbol.for("react.suspense"),ya=Symbol.for("react.memo"),za=Symbol.for("react.lazy"),V=Symbol.iterator,X={isMounted:function(a){return!1},enqueueForceUpdate:function(a,b,c){},enqueueReplaceState:function(a,b,c,m){},enqueueSetState:function(a,b,c,m){}},ka=Object.assign,W={};w.prototype.isReactComponent={};w.prototype.setState=function(a,b){if("object"!==typeof a&&"function"!==typeof a&&null!=a)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");
|
||||
this.updater.enqueueSetState(this,a,b,"setState")};w.prototype.forceUpdate=function(a){this.updater.enqueueForceUpdate(this,a,"forceUpdate")};Y.prototype=w.prototype;var t=K.prototype=new Y;t.constructor=K;ka(t,w.prototype);t.isPureReactComponent=!0;var ca=Array.isArray,aa=Object.prototype.hasOwnProperty,L={current:null},ba={key:!0,ref:!0,__self:!0,__source:!0},da=/\/+/g,g={current:null},J={transition:null};if("object"===typeof performance&&"function"===typeof performance.now){var Aa=performance;
|
||||
var v=function(){return Aa.now()}}else{var la=Date,Ba=la.now();v=function(){return la.now()-Ba}}var q=[],r=[],Ca=1,n=null,k=3,F=!1,u=!1,z=!1,ja="function"===typeof setTimeout?setTimeout:null,ea="function"===typeof clearTimeout?clearTimeout:null,ma="undefined"!==typeof setImmediate?setImmediate:null;"undefined"!==typeof navigator&&void 0!==navigator.scheduling&&void 0!==navigator.scheduling.isInputPending&&navigator.scheduling.isInputPending.bind(navigator.scheduling);var H=!1,G=null,A=-1,ia=5,ha=
|
||||
-1,U=function(){if(null!==G){var a=v();ha=a;var b=!0;try{b=G(!0,a)}finally{b?I():(H=!1,G=null)}}else H=!1};if("function"===typeof ma)var I=function(){ma(U)};else if("undefined"!==typeof MessageChannel){t=new MessageChannel;var Da=t.port2;t.port1.onmessage=U;I=function(){Da.postMessage(null)}}else I=function(){ja(U,0)};t={ReactCurrentDispatcher:g,ReactCurrentOwner:L,ReactCurrentBatchConfig:J,Scheduler:{__proto__:null,unstable_ImmediatePriority:1,unstable_UserBlockingPriority:2,unstable_NormalPriority:3,
|
||||
unstable_IdlePriority:5,unstable_LowPriority:4,unstable_runWithPriority:function(a,b){switch(a){case 1:case 2:case 3:case 4:case 5:break;default:a=3}var c=k;k=a;try{return b()}finally{k=c}},unstable_next:function(a){switch(k){case 1:case 2:case 3:var b=3;break;default:b=k}var c=k;k=b;try{return a()}finally{k=c}},unstable_scheduleCallback:function(a,b,c){var e=v();"object"===typeof c&&null!==c?(c=c.delay,c="number"===typeof c&&0<c?e+c:e):c=e;switch(a){case 1:var d=-1;break;case 2:d=250;break;case 5:d=
|
||||
1073741823;break;case 4:d=1E4;break;default:d=5E3}d=c+d;a={id:Ca++,callback:b,priorityLevel:a,startTime:c,expirationTime:d,sortIndex:-1};c>e?(a.sortIndex=c,O(r,a),null===p(q)&&a===p(r)&&(z?(ea(A),A=-1):z=!0,T(Q,c-e))):(a.sortIndex=d,O(q,a),u||F||(u=!0,R(S)));return a},unstable_cancelCallback:function(a){a.callback=null},unstable_wrapCallback:function(a){var b=k;return function(){var c=k;k=b;try{return a.apply(this,arguments)}finally{k=c}}},unstable_getCurrentPriorityLevel:function(){return k},unstable_shouldYield:fa,
|
||||
unstable_requestPaint:function(){},unstable_continueExecution:function(){u||F||(u=!0,R(S))},unstable_pauseExecution:function(){},unstable_getFirstCallbackNode:function(){return p(q)},get unstable_now(){return v},unstable_forceFrameRate:function(a){0>a||125<a?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):ia=0<a?Math.floor(1E3/a):5},unstable_Profiling:null}};c.Children={map:C,forEach:function(a,b,c){C(a,function(){b.apply(this,
|
||||
arguments)},c)},count:function(a){var b=0;C(a,function(){b++});return b},toArray:function(a){return C(a,function(a){return a})||[]},only:function(a){if(!M(a))throw Error("React.Children.only expected to receive a single React element child.");return a}};c.Component=w;c.Fragment=ra;c.Profiler=ta;c.PureComponent=K;c.StrictMode=sa;c.Suspense=xa;c.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=t;c.cloneElement=function(a,b,c){if(null===a||void 0===a)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+
|
||||
a+".");var e=ka({},a.props),d=a.key,k=a.ref,h=a._owner;if(null!=b){void 0!==b.ref&&(k=b.ref,h=L.current);void 0!==b.key&&(d=""+b.key);if(a.type&&a.type.defaultProps)var l=a.type.defaultProps;for(f in b)aa.call(b,f)&&!ba.hasOwnProperty(f)&&(e[f]=void 0===b[f]&&void 0!==l?l[f]:b[f])}var f=arguments.length-2;if(1===f)e.children=c;else if(1<f){l=Array(f);for(var g=0;g<f;g++)l[g]=arguments[g+2];e.children=l}return{$$typeof:y,type:a.type,key:d,ref:k,props:e,_owner:h}};c.createContext=function(a){a={$$typeof:va,
|
||||
_currentValue:a,_currentValue2:a,_threadCount:0,Provider:null,Consumer:null,_defaultValue:null,_globalName:null};a.Provider={$$typeof:ua,_context:a};return a.Consumer=a};c.createElement=Z;c.createFactory=function(a){var b=Z.bind(null,a);b.type=a;return b};c.createRef=function(){return{current:null}};c.forwardRef=function(a){return{$$typeof:wa,render:a}};c.isValidElement=M;c.lazy=function(a){return{$$typeof:za,_payload:{_status:-1,_result:a},_init:qa}};c.memo=function(a,b){return{$$typeof:ya,type:a,
|
||||
compare:void 0===b?null:b}};c.startTransition=function(a,b){b=J.transition;J.transition={};try{a()}finally{J.transition=b}};c.unstable_act=function(a){throw Error("act(...) is not supported in production builds of React.");};c.useCallback=function(a,b){return g.current.useCallback(a,b)};c.useContext=function(a){return g.current.useContext(a)};c.useDebugValue=function(a,b){};c.useDeferredValue=function(a){return g.current.useDeferredValue(a)};c.useEffect=function(a,b){return g.current.useEffect(a,
|
||||
b)};c.useId=function(){return g.current.useId()};c.useImperativeHandle=function(a,b,c){return g.current.useImperativeHandle(a,b,c)};c.useInsertionEffect=function(a,b){return g.current.useInsertionEffect(a,b)};c.useLayoutEffect=function(a,b){return g.current.useLayoutEffect(a,b)};c.useMemo=function(a,b){return g.current.useMemo(a,b)};c.useReducer=function(a,b,c){return g.current.useReducer(a,b,c)};c.useRef=function(a){return g.current.useRef(a)};c.useState=function(a){return g.current.useState(a)};
|
||||
c.useSyncExternalStore=function(a,b,c){return g.current.useSyncExternalStore(a,b,c)};c.useTransition=function(){return g.current.useTransition()};c.version="18.2.0"});
|
||||
})();
|
||||
+83
File diff suppressed because one or more lines are too long
@@ -15,29 +15,28 @@ class FluidSynthBridge extends AudioWorkletProcessor {
|
||||
|
||||
process(inputs, outputs) {
|
||||
const out = outputs[0];
|
||||
if (!out) return true;
|
||||
if (!out || out.length === 0) return true;
|
||||
this.called++;
|
||||
const numCh = out.length;
|
||||
const len = out[0].length;
|
||||
const qL = this.leftQ;
|
||||
const qR = this.rightQ;
|
||||
let fi = 0;
|
||||
let si = 0;
|
||||
// Handle any output channel count (mono devices produce 1 channel, so
|
||||
// out[1] may be undefined — never write into a missing channel).
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (fi >= qL.length) { out[0][i] = 0; out[1][i] = 0; continue; }
|
||||
out[0][i] = qL[fi][si];
|
||||
out[1][i] = qR[fi][si];
|
||||
if (fi >= qL.length) {
|
||||
for (let c = 0; c < numCh; c++) out[c][i] = 0;
|
||||
continue;
|
||||
}
|
||||
for (let c = 0; c < numCh; c++) {
|
||||
out[c][i] = c % 2 === 0 ? qL[fi][si] : qR[fi][si];
|
||||
}
|
||||
si++;
|
||||
if (si >= qL[fi].length) { fi++; si = 0; }
|
||||
}
|
||||
if (fi > 0) { this.leftQ.splice(0, fi); this.rightQ.splice(0, fi); }
|
||||
if (this.called % 50 === 0) {
|
||||
var pk = 0;
|
||||
for (var j = 0; j < len; j++) {
|
||||
var v = out[0][j] > 0 ? out[0][j] : -out[0][j];
|
||||
if (v > pk) pk = v;
|
||||
}
|
||||
if (pk > 0) console.log('[FluidSynth:bridge] process #' + this.called + ' peak:' + pk.toFixed(6) + ' q:' + qL.length);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
8d26e2b55e73579d1bb3c37b4878f1845ef9cbf50a8e4ee6f7deaa2ab80db32d
|
||||
@@ -6,24 +6,25 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SonicForge Studio - Professional DAW Editor</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
|
||||
<script src="/static/js/vendor/tailwindcss-cdn.js"></script>
|
||||
<script src="/static/js/vendor/lucide.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||
<script src="/static/js/vendor/react.production.min.js"></script>
|
||||
<script src="/static/js/vendor/react-dom.production.min.js"></script>
|
||||
<script src="/static/js/services/fluidsynthLoader.js?v=202607271245"></script>
|
||||
<script src="/static/js/services/api.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/api.js?v=202608080950"></script>
|
||||
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/storage.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/storage.js?v=202608038200"></script>
|
||||
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/soundfontPlayer.js?v=202607271245"></script>
|
||||
<script src="/static/js/services/aiGateway.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/soundfontPlayer.js?v=202608070635"></script>
|
||||
<script src="/static/js/services/aiGateway.js?v=202608037200"></script>
|
||||
<script src="/static/js/services/dawCommandDispatcher.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/pianoRollTabService.js?v=202607272044"></script>
|
||||
<script src="/static/js/services/ghostNoteExtractor.js?v=202607271727"></script>
|
||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202607302132" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608081555" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
@@ -380,6 +381,32 @@
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.9), inset 0 1px 1px rgba(255,255,255,0.9);
|
||||
}
|
||||
|
||||
/* Media Explorer: horizontal volume slider + selected file row */
|
||||
input[type=range].me-fader-slider {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: #111;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #555;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type=range].me-fader-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
height: 16px;
|
||||
width: 12px;
|
||||
background: linear-gradient(180deg, #e2e8f0 0%, #64748b 50%, #1e293b 100%);
|
||||
border: 1px solid #000;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.5);
|
||||
cursor: pointer;
|
||||
}
|
||||
.file-row-selected {
|
||||
background-color: #3399ff !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.file-row-selected .file-icon { color: #ffffff !important; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/app")
|
||||
|
||||
try:
|
||||
import fluidsynth
|
||||
print("fluidsynth import successful.")
|
||||
|
||||
fl = fluidsynth.Synth()
|
||||
# Try to load a pre-existing system SF3
|
||||
sf3_path = "/opt/daw_engine/soundfonts/Equinox_Grand_Pianos.sf3"
|
||||
print(f"Checking if {sf3_path} exists: {os.path.exists(sf3_path)}")
|
||||
if os.path.exists(sf3_path):
|
||||
h = fl.sfload(sf3_path)
|
||||
print(f"Loaded {sf3_path}, handle: {h}")
|
||||
else:
|
||||
print("Equinox_Grand_Pianos.sf3 not found.")
|
||||
except Exception as e:
|
||||
print(f"Failed: {e}")
|
||||
@@ -0,0 +1,66 @@
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
|
||||
# Ensure app is in path
|
||||
sys.path.insert(0, "/app")
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger("test_sf_convert")
|
||||
|
||||
from app.core.soundfont_converter import SoundFontConverter
|
||||
|
||||
def test():
|
||||
sf2_dir = "/app/app/storage/soundfonts"
|
||||
sf2_files = [os.path.join(sf2_dir, f) for f in os.listdir(sf2_dir) if f.endswith(".sf2") and "_decomp" not in f]
|
||||
if not sf2_files:
|
||||
logger.error("No SF2 files found in /app/app/storage/soundfonts")
|
||||
return
|
||||
|
||||
sf2_path = sf2_files[0]
|
||||
logger.info(f"Testing with SF2 file: {sf2_path}")
|
||||
|
||||
converter = SoundFontConverter()
|
||||
|
||||
# Check ffmpeg encoder support
|
||||
has_ogg = converter._check_ffmpeg_ogg()
|
||||
logger.info(f"ffmpeg with libvorbis available: {has_ogg}")
|
||||
|
||||
# Convert SF2 -> SF3
|
||||
sf3_path = sf2_path.replace(".sf2", ".sf3")
|
||||
if os.path.exists(sf3_path):
|
||||
os.remove(sf3_path)
|
||||
|
||||
logger.info("Converting SF2 -> SF3...")
|
||||
result_path = converter.convert_sf2_to_sf3(sf2_path)
|
||||
logger.info(f"Result path from convert_sf2_to_sf3: {result_path}")
|
||||
|
||||
if result_path.endswith(".sf3"):
|
||||
logger.info(f"SF3 file exists: {os.path.exists(sf3_path)}")
|
||||
if os.path.exists(sf3_path):
|
||||
logger.info(f"SF3 size: {os.path.getsize(sf3_path)} bytes")
|
||||
# Verify if it plays audio
|
||||
plays = converter._sf3_plays_audio(sf3_path)
|
||||
logger.info(f"SF3 plays audio (pyfluidsynth verify): {plays}")
|
||||
|
||||
# Now test decompression back to SF2
|
||||
decomp_sf2 = sf3_path.replace(".sf3", "_decomp.sf2")
|
||||
if os.path.exists(decomp_sf2):
|
||||
os.remove(decomp_sf2)
|
||||
|
||||
logger.info("Decompressing SF3 -> SF2...")
|
||||
try:
|
||||
decomp_result = converter.sf3_to_sf2(sf3_path, decomp_sf2)
|
||||
logger.info(f"Decompress result path: {decomp_result}")
|
||||
if os.path.exists(decomp_sf2):
|
||||
logger.info(f"Decompressed SF2 size: {os.path.getsize(decomp_sf2)} bytes")
|
||||
# Check if it plays
|
||||
decomp_plays = converter._sf3_plays_audio(decomp_sf2)
|
||||
logger.info(f"Decompressed SF2 plays audio: {decomp_plays}")
|
||||
except Exception as e:
|
||||
logger.error(f"Decompression failed: {e}", exc_info=True)
|
||||
else:
|
||||
logger.warning("Conversion did not produce an SF3 path.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test()
|
||||
@@ -0,0 +1,14 @@
|
||||
// Rebuild app.precompiled.js from app.jsx using @babel/standalone (avoids the
|
||||
// Babel 8 ESM-only CLI conflict). Mirrors package.json's build script:
|
||||
// babel app/static/js/app.jsx --config-file ./babel.config.json -o app/static/js/app.precompiled.js
|
||||
import * as Babel from '@babel/standalone';
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
|
||||
const src = readFileSync('app/static/js/app.jsx', 'utf8');
|
||||
const out = Babel.transform(src, {
|
||||
presets: ['react'],
|
||||
filename: 'app.jsx',
|
||||
sourceType: 'script',
|
||||
}).code;
|
||||
writeFileSync('app/static/js/app.precompiled.js', out);
|
||||
console.log('BUILD OK', out.length, 'bytes');
|
||||
@@ -0,0 +1,85 @@
|
||||
@echo off
|
||||
REM SonicForge DAW - Windows build (daw_engine.exe + Tauri shell)
|
||||
setlocal enabledelayedexpansion
|
||||
cd /d "%~dp0"
|
||||
|
||||
echo [1/6] Kiem tra Python + Node...
|
||||
python --version
|
||||
if errorlevel 1 (
|
||||
echo THIEU Python 3.11+ - cai tu python.org, tick Add to PATH
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
node --version
|
||||
if errorlevel 1 (
|
||||
echo THIEU Node 20+ - cai tu nodejs.org
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [2/6] Kiem tra / cai Rust toolchain...
|
||||
where cargo >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo Cai Rust qua rustup MSVC toolchain...
|
||||
powershell -Command "Invoke-WebRequest https://win.rustup.rs/x86_64 -OutFile rustup-init.exe"
|
||||
rustup-init.exe -y --default-toolchain stable --profile minimal
|
||||
del rustup-init.exe
|
||||
set "PATH=%USERPROFILE%\.cargo\bin;%PATH%"
|
||||
)
|
||||
cargo --version
|
||||
if errorlevel 1 (
|
||||
echo Rust cai that bai - can Visual Studio Build Tools voi C++ workload
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [3/6] Cai Python deps...
|
||||
pip install pyinstaller pywin32
|
||||
if errorlevel 1 exit /b 1
|
||||
pip install -r requirements.txt
|
||||
if errorlevel 1 exit /b 1
|
||||
pip install --no-deps "pedalboard>=0.8.0"
|
||||
|
||||
echo [4/6] Precompile JS...
|
||||
npm install --no-audit --no-fund
|
||||
if errorlevel 1 (
|
||||
echo FAIL: npm install that bai - kiem tra internet / package.json
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo - npm OK, kiem tra babel...
|
||||
if not exist "node_modules\@babel\cli\bin\babel.js" (
|
||||
echo FAIL: thieu node_modules\@babel\cli\bin\babel.js - npm install khong hoan tat
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo - chay Babel...
|
||||
node node_modules/@babel/cli/bin/babel.js app/static/js/app.jsx --config-file ./babel.config.json -o app/static/js/app.precompiled.js
|
||||
if errorlevel 1 (
|
||||
echo FAIL: Babel precompile that bai - xem loi phia tren
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo - Babel OK
|
||||
|
||||
echo [5/6] Build daw_engine.exe...
|
||||
pyinstaller engine.spec --clean --noconfirm
|
||||
if errorlevel 1 exit /b 1
|
||||
if not exist "src-tauri\binaries" mkdir "src-tauri\binaries"
|
||||
copy /y dist\daw_engine.exe "src-tauri\binaries\daw_engine-x86_64-pc-windows-msvc.exe" >nul
|
||||
|
||||
echo [6/6] Build Tauri installer...
|
||||
taskkill /F /T /IM daw_engine.exe >nul 2>nul
|
||||
npm install -D @tauri-apps/cli@^2
|
||||
if errorlevel 1 (
|
||||
echo npm install tauri-cli that bai
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
call npx tauri build
|
||||
|
||||
echo.
|
||||
echo HOAN TAT - Installer:
|
||||
echo src-tauri\target\release\bundle\nsis\SonicForgeDAW_1.0.0_x64-setup.exe
|
||||
echo src-tauri\target\release\bundle\msi\SonicForgeDAW_1.0.0_x64_en-US.msi
|
||||
pause
|
||||
@@ -0,0 +1,55 @@
|
||||
# ── Production compose — KHÔNG mount source, image từ registry, bí mật qua .env ──
|
||||
# Dùng: docker compose -f docker-compose.prod.yml up -d
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
|
||||
web:
|
||||
build: .
|
||||
image: ${IMAGE:-sonicforge-studio:latest}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${WEB_PORT:-8000}:8000"
|
||||
env_file: .env
|
||||
volumes:
|
||||
- sf_data:/app/app/storage
|
||||
- ${VST3_DIR:-./vst_plugins}:/opt/daw_engine/vst3:ro
|
||||
- ${SOUNDFONTS_DIR:-./soundfonts}:/opt/daw_engine/soundfonts:ro
|
||||
- ${PIANOBOOK_DIR:-./samples/pianobook}:/opt/daw_engine/samples/pianobook:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request;urllib.request.urlopen('http://localhost:8000/health')"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
worker:
|
||||
build: .
|
||||
image: ${IMAGE:-sonicforge-studio:latest}
|
||||
restart: unless-stopped
|
||||
command: celery -A app.tasks.worker.celery_app worker --loglevel=info
|
||||
env_file: .env
|
||||
volumes:
|
||||
- sf_data:/app/app/storage
|
||||
- ${VST3_DIR:-./vst_plugins}:/opt/daw_engine/vst3:ro
|
||||
- ${SOUNDFONTS_DIR:-./soundfonts}:/opt/daw_engine/soundfonts:ro
|
||||
- ${PIANOBOOK_DIR:-./samples/pianobook}:/opt/daw_engine/samples/pianobook:ro
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
beat:
|
||||
build: .
|
||||
image: ${IMAGE:-sonicforge-studio:latest}
|
||||
restart: unless-stopped
|
||||
command: celery -A app.tasks.worker.celery_app beat --loglevel=info
|
||||
env_file: .env
|
||||
volumes:
|
||||
- sf_data:/app/app/storage
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
volumes:
|
||||
sf_data:
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
# engine.spec — PyInstaller config (spec desktop 2026-08-08 — daw_engine.exe)
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import os
|
||||
from PyInstaller.utils.hooks import collect_dynamic_libs, collect_data_files
|
||||
|
||||
block_cipher = None
|
||||
|
||||
# Collect native DLL files for pedalboard and soundfile
|
||||
binaries = collect_dynamic_libs('pedalboard')
|
||||
binaries += collect_dynamic_libs('soundfile')
|
||||
binaries += collect_dynamic_libs('fluidsynth')
|
||||
|
||||
# Collect configuration files and default soundfont catalogs
|
||||
datas = [
|
||||
('app/storage', 'app/storage'),
|
||||
('app/static', 'app/static'),
|
||||
('app/templates', 'app/templates'),
|
||||
]
|
||||
|
||||
a = Analysis(
|
||||
['main.py'],
|
||||
pathex=[],
|
||||
binaries=binaries,
|
||||
datas=datas,
|
||||
hiddenimports=[
|
||||
'uvicorn.logging',
|
||||
'uvicorn.loops',
|
||||
'uvicorn.loops.auto',
|
||||
'uvicorn.protocols',
|
||||
'uvicorn.protocols.http',
|
||||
'uvicorn.protocols.http.auto',
|
||||
'uvicorn.protocols.websockets',
|
||||
'uvicorn.protocols.websockets.auto',
|
||||
'pedalboard',
|
||||
'soundfile',
|
||||
'sf2utils',
|
||||
'mido.backends.rtmidi',
|
||||
'app.main',
|
||||
'app.config',
|
||||
'app.api.v1.desktop_plugins',
|
||||
'app.core.vst_scanner',
|
||||
'app.core.soundfont_scanner',
|
||||
# Celery dynamic imports (PyInstaller không bắt được — 2026-08-08)
|
||||
'celery.fixups',
|
||||
'celery.fixups.django',
|
||||
'celery.backends',
|
||||
'celery.backends.redis',
|
||||
'celery.schedules',
|
||||
'celery.app.builtins',
|
||||
'kombu.transport.redis',
|
||||
'billiard',
|
||||
'vine',
|
||||
'redis',
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='daw_engine',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False, # Set to True for Debugging, False for Production
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Entry point cho daw_engine.exe (PyInstaller — spec desktop 2026-08-08).
|
||||
Chạy FastAPI backend trên 127.0.0.1:8000 (Dual-Process Desktop Architecture).
|
||||
KHÔNG tự mở browser — Tauri shell (window url=localhost:8000) đảm nhiệm.
|
||||
SINGLE-INSTANCE: nếu port 8000 đã có server → EXIT ngay (bug 09:55 — mỗi lần mở
|
||||
app spawn thêm daw_engine → nhiều instance + port chiếm → màn đen)."""
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
|
||||
os.environ.setdefault("SFDATA_DIR", os.path.join(os.path.expanduser("~"), "SonicForgeDAW"))
|
||||
|
||||
import uvicorn # noqa: E402
|
||||
|
||||
PORT = 8000
|
||||
|
||||
|
||||
def _port_in_use(port: int) -> bool:
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
s.bind(("127.0.0.1", port))
|
||||
return False
|
||||
except OSError:
|
||||
return True
|
||||
finally:
|
||||
s.close()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if _port_in_use(PORT):
|
||||
# Server đang chạy (instance khác — window sẽ dùng nó). KHÔNG spawn thêm.
|
||||
sys.exit(0)
|
||||
uvicorn.run("app.main:app", host="127.0.0.1", port=PORT, log_level="warning")
|
||||
@@ -0,0 +1,349 @@
|
||||
# DETAILED SPECIFICATION FOR DAW MEDIA EXPLORER PANEL INTERFACE & HTML5 SOURCE CODE
|
||||
|
||||
This document details the design structure, features, and executable HTML5 source code for a general-purpose Media Explorer Panel interface styled after the REAPER DAW.
|
||||
|
||||
---
|
||||
|
||||
## I. DETAILED INTERFACE REGIONS
|
||||
|
||||
The interface is divided vertically into 3 primary functional regions, arranged from top to bottom:
|
||||
|
||||
### 1. Top Navigation Toolbar
|
||||
|
||||
* **Left Navigation Button Group:** **←** (Back), **→** (Forward), **↑** (Up to Parent Directory), **↻** (Refresh).
|
||||
* **Directory Address Bar:** Displays the current folder path with a drop-down menu for quick-access path selection.
|
||||
* **Filter / Search Box:** Input field supporting quick file filtering and keyword searching.
|
||||
* **View Mode Button:** Toggle button for switching file display layouts (*Details* / *List* view).
|
||||
|
||||
### 2. Middle Split Panel (Directory Tree + File List)
|
||||
|
||||
* **Left Directory Tree View:**
|
||||
* Hierarchical tree structure displaying project directories, system shortcuts, drives, and audio sample library folders.
|
||||
* Supports expand/collapse toggle buttons and highlighted background states indicating the actively selected folder.
|
||||
|
||||
|
||||
* **Right File List Table:**
|
||||
* Displays audio and MIDI files with standard category icons.
|
||||
* Highlights the currently selected file with a prominent blue row background.
|
||||
|
||||
|
||||
|
||||
### 3. Bottom Preview & Transport Panel
|
||||
|
||||
* **Transport Control Bar & Playback Parameters:**
|
||||
* **Transport Buttons:** **■** (Stop), **▶** (Play), **❚❚** (Pause), **↻** (Loop/Repeat), **⚡ Auto-Play** (Automatically previews files upon selection).
|
||||
* **Parameter Controls:** Pitch adjustment (Pitch matching), Rate (Playback speed factor), Volume Slider (Gain adjustment in dB).
|
||||
* **Media Type Badge:** Label displaying file format classification (*MIDI* / *Audio*).
|
||||
|
||||
|
||||
* **Visualizer Canvas Display & Metadata:**
|
||||
* **Visualizer Canvas (Left):** Dark display area rendering an overview Piano Roll note grid (for MIDI files) or waveform display (for Audio files), integrated with a timeline/bar ruler and white Playhead cursor.
|
||||
* **Metadata Text Box (Right):** Information pane displaying detailed file parameters (MIDI event count, duration, sample rate / resolution).
|
||||
|
||||
|
||||
* **Footer Status Bar:**
|
||||
* Displays current playback timestamp vs. total file length.
|
||||
* Name of the currently selected/playing file.
|
||||
* Tempo metadata (BPM) and playback rate scale factor.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## II. EXECUTABLE HTML5 & TAILWIND CSS SOURCE CODE
|
||||
|
||||
Below is the complete HTML5 source code integrating Web Audio API synthesis, a Canvas visualizer, and real-time interactive file selection and audio preview playback:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DAW Media Explorer Panel Component</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; user-select: none; }
|
||||
.font-mono { font-family: 'JetBrains Mono', monospace; }
|
||||
|
||||
/* REAPER Classic Panel Style */
|
||||
.reaper-panel {
|
||||
background: #c0c0c0;
|
||||
border: 2px solid #ffffff;
|
||||
border-right-color: #808080;
|
||||
border-bottom-color: #808080;
|
||||
}
|
||||
.reaper-inset {
|
||||
background: #ffffff;
|
||||
border: 1px solid #808080;
|
||||
box-shadow: inset 1px 1px 2px rgba(0,0,0,0.3);
|
||||
}
|
||||
.reaper-dark-inset {
|
||||
background: #181818;
|
||||
border: 1px solid #3a3a3a;
|
||||
box-shadow: inset 1px 1px 3px rgba(0,0,0,0.8);
|
||||
}
|
||||
.file-row-selected {
|
||||
background-color: #3399ff !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.file-row-selected .file-icon { color: #ffffff !important; }
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar { width: 12px; height: 12px; }
|
||||
::-webkit-scrollbar-track { background: #e0e0e0; border-left: 1px solid #a0a0a0; }
|
||||
::-webkit-scrollbar-thumb { background: #b0b0b0; border: 1px solid #ffffff; border-right-color: #707070; border-bottom-color: #707070; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #909090; }
|
||||
|
||||
/* Volume Slider */
|
||||
input[type=range].fader-slider {
|
||||
appearance: none; -webkit-appearance: none;
|
||||
background: #111; height: 6px; border-radius: 3px; border: 1px solid #555;
|
||||
}
|
||||
input[type=range].fader-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none; height: 16px; width: 12px;
|
||||
background: linear-gradient(180deg, #e2e8f0 0%, #64748b 50%, #1e293b 100%);
|
||||
border: 1px solid #000; border-radius: 2px; box-shadow: 0 2px 4px rgba(0,0,0,0.5); cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-950 text-slate-900 h-screen flex items-center justify-center p-2 overflow-hidden">
|
||||
|
||||
<!-- MEDIA EXPLORER PANEL CONTAINER -->
|
||||
<div class="w-full max-w-4xl h-[420px] flex flex-col reaper-panel text-slate-900 overflow-hidden shadow-2xl relative">
|
||||
|
||||
<!-- 1. TOP NAVIGATION TOOLBAR -->
|
||||
<div class="h-8 bg-[#d4d0c8] border-b border-[#808080] px-2 flex items-center justify-between text-xs shrink-0">
|
||||
<div class="flex items-center gap-1 flex-1 mr-2">
|
||||
<button class="w-5 h-5 bg-[#e0e0e0] hover:bg-[#ffffff] border border-[#707070] rounded-sm flex items-center justify-center text-[10px]" title="Back">
|
||||
<i class="fa-solid fa-arrow-left"></i>
|
||||
</button>
|
||||
<button class="w-5 h-5 bg-[#e0e0e0] hover:bg-[#ffffff] border border-[#707070] rounded-sm flex items-center justify-center text-[10px]" title="Forward">
|
||||
<i class="fa-solid fa-arrow-right"></i>
|
||||
</button>
|
||||
<button class="w-5 h-5 bg-[#e0e0e0] hover:bg-[#ffffff] border border-[#707070] rounded-sm flex items-center justify-center text-[10px]" title="Up Directory">
|
||||
<i class="fa-solid fa-arrow-up"></i>
|
||||
</button>
|
||||
<button class="w-5 h-5 bg-[#e0e0e0] hover:bg-[#ffffff] border border-[#707070] rounded-sm flex items-center justify-center text-[10px]" title="Refresh">
|
||||
<i class="fa-solid fa-rotate-right"></i>
|
||||
</button>
|
||||
|
||||
<div class="flex-1 flex items-center reaper-inset h-5 px-1 bg-white">
|
||||
<i class="fa-solid fa-folder-open text-[#d9a752] text-[11px] mr-1.5"></i>
|
||||
<input type="text" id="addressBarInput" value="Root:\Media Library" class="w-full text-xs outline-none bg-transparent font-sans text-slate-800" readonly>
|
||||
<i class="fa-solid fa-caret-down text-slate-600 text-[10px] ml-1"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center reaper-inset h-5 px-1 bg-white w-40">
|
||||
<input type="text" placeholder="Filter/Search..." class="w-full text-xs outline-none bg-transparent font-sans text-slate-800">
|
||||
<i class="fa-solid fa-caret-down text-slate-600 text-[10px] ml-1"></i>
|
||||
</div>
|
||||
<button class="px-2 py-0.5 bg-[#e0e0e0] hover:bg-[#ffffff] border border-[#707070] rounded-sm text-[11px] flex items-center gap-1 font-semibold">
|
||||
<span>Details</span> <i class="fa-solid fa-caret-down text-[9px]"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. MIDDLE SPLIT VIEW (DIRECTORY TREE + FILE LIST) -->
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
|
||||
<!-- DIRECTORY TREE (LEFT) -->
|
||||
<div class="w-60 reaper-inset m-1 mr-0 overflow-y-auto p-1 text-xs select-none bg-white">
|
||||
<div class="space-y-0.5 font-sans">
|
||||
<div class="flex items-center gap-1 px-1 py-0.5 hover:bg-slate-200 text-slate-700"><span class="w-3"></span> <Track Templates></div>
|
||||
<div class="flex items-center gap-1 px-1 py-0.5 hover:bg-slate-200 text-slate-700"><span class="w-3"></span> <Project Directory></div>
|
||||
<div class="flex items-center gap-1 px-1 py-0.5 hover:bg-slate-200 text-slate-700"><i class="fa-solid fa-plus text-[9px] text-slate-500"></i> My Computer</div>
|
||||
|
||||
<!-- Selected Folder -->
|
||||
<div id="folderMidi" class="flex items-center gap-1 px-1 py-0.5 hover:bg-blue-100 cursor-pointer font-semibold text-slate-900 bg-slate-300 rounded-sm">
|
||||
<i class="fa-solid fa-minus text-[9px] text-slate-600"></i>
|
||||
<i class="fa-solid fa-folder-open text-[#d9a752]"></i> Media Library
|
||||
</div>
|
||||
|
||||
<div class="pl-3 space-y-0.5">
|
||||
<div class="flex items-center gap-1 px-1 py-0.5 hover:bg-slate-200 cursor-pointer text-slate-800">
|
||||
<i class="fa-solid fa-plus text-[9px] text-slate-500"></i> <i class="fa-solid fa-folder text-[#d9a752]"></i> Sound Effects
|
||||
</div>
|
||||
<div id="folderDigitalJuice" class="flex items-center gap-1 px-1 py-0.5 hover:bg-blue-100 cursor-pointer text-slate-800 pl-4">
|
||||
<i class="fa-solid fa-folder text-[#d9a752]"></i> MIDI Collections
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1 px-1 py-0.5 hover:bg-slate-200 text-slate-700"><i class="fa-solid fa-plus text-[9px] text-slate-500"></i> Desktop</div>
|
||||
<div class="flex items-center gap-1 px-1 py-0.5 hover:bg-slate-200 text-slate-700"><i class="fa-solid fa-plus text-[9px] text-slate-500"></i> My Documents</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FILE LIST TABLE (RIGHT) -->
|
||||
<div class="flex-1 reaper-inset m-1 overflow-y-auto relative bg-white">
|
||||
<table class="w-full text-xs text-left border-collapse">
|
||||
<thead class="sticky top-0 bg-[#e0e0e0] border-b border-[#a0a0a0] text-slate-800 font-semibold select-none shadow-sm z-10">
|
||||
<tr><th class="py-1 px-2 border-r border-[#b0b0b0]">File</th></tr>
|
||||
</thead>
|
||||
<tbody id="fileListTbody" class="font-sans text-slate-800">
|
||||
<!-- Dynamic File Rows -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 3. BOTTOM PREVIEW CONTROL BAR & VISUALIZER CANVAS -->
|
||||
<div class="h-32 bg-[#d4d0c8] border-t border-[#808080] p-1.5 flex flex-col justify-between text-xs shrink-0 select-none">
|
||||
|
||||
<!-- CONTROLS ROW -->
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-1">
|
||||
<button id="btnStop" class="w-6 h-6 bg-[#e0e0e0] hover:bg-[#ffffff] border border-[#707070] rounded-sm flex items-center justify-center text-slate-800"><i class="fa-solid fa-square text-[10px]"></i></button>
|
||||
<button id="btnPlay" class="w-6 h-6 bg-[#e0e0e0] hover:bg-[#ffffff] border border-[#707070] rounded-sm flex items-center justify-center text-emerald-700 font-bold"><i class="fa-solid fa-play text-xs"></i></button>
|
||||
<button id="btnPause" class="w-6 h-6 bg-[#e0e0e0] hover:bg-[#ffffff] border border-[#707070] rounded-sm flex items-center justify-center text-amber-700"><i class="fa-solid fa-pause text-xs"></i></button>
|
||||
<button id="btnLoop" class="w-6 h-6 bg-[#e0e0e0] hover:bg-[#ffffff] border border-[#707070] rounded-sm flex items-center justify-center text-slate-700"><i class="fa-solid fa-rotate-right text-xs"></i></button>
|
||||
<button id="btnAutoPlay" class="h-6 px-2 bg-gradient-to-r from-cyan-600 to-emerald-600 text-white border border-slate-700 rounded-sm font-bold text-[10px] flex items-center gap-1 shadow-sm">
|
||||
<i class="fa-solid fa-bolt text-[9px]"></i> <span>Auto-Play</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 font-mono text-[11px]">
|
||||
<div class="flex items-center gap-1">
|
||||
<span>Pitch:</span>
|
||||
<div class="reaper-inset px-1 bg-white h-5 flex items-center w-14">
|
||||
<input type="number" value="0.00" step="0.5" class="w-full text-xs text-right outline-none bg-transparent">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<span>Rate:</span>
|
||||
<div class="reaper-inset px-1 bg-white h-5 flex items-center w-12">
|
||||
<input type="number" value="1.0" step="0.1" class="w-full text-xs text-right outline-none bg-transparent">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-sans text-slate-700">Volume:</span>
|
||||
<input id="volSlider" type="range" min="-60" max="12" step="0.5" value="0" class="fader-slider w-28">
|
||||
<div class="reaper-inset px-1 bg-white h-5 flex items-center justify-center w-14 font-mono text-[11px]">
|
||||
<span id="volDbText">0.00 dB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="badgeType" class="px-2 py-0.5 bg-purple-950 text-purple-300 border border-purple-800 font-mono font-bold text-[10px] rounded-sm">
|
||||
MIDI
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CANVAS & METADATA ROW -->
|
||||
<div class="flex items-stretch gap-2 my-1 h-16">
|
||||
<div class="flex-1 reaper-dark-inset relative overflow-hidden">
|
||||
<canvas id="previewCanvas" class="w-full h-full block cursor-pointer"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="w-56 reaper-dark-inset p-1.5 font-mono text-[10px] text-slate-300 leading-tight overflow-y-auto">
|
||||
<div id="metadataContent">
|
||||
76 MIDI events<br>
|
||||
Length: 16 quarter notes<br>
|
||||
Length: 0:08.000 (est)<br>
|
||||
Ticks per quarter note: 480
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BOTTOM STATUS BAR -->
|
||||
<div class="h-5 bg-[#c0c0c0] border-t border-[#ffffff] flex items-center justify-between text-[11px] font-mono px-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<div id="timeRange" class="reaper-inset px-1.5 bg-white text-slate-900 font-bold">0.0000 / 16.0000</div>
|
||||
<div class="reaper-inset px-1.5 bg-white text-slate-900">0.0000</div>
|
||||
<div class="reaper-inset px-1.5 bg-white text-slate-900">16.0000</div>
|
||||
</div>
|
||||
|
||||
<div id="statusFileName" class="text-slate-800 font-bold truncate max-w-xs">File_Selected.mid</div>
|
||||
<div id="statusBpm" class="text-slate-700">130 bpm x0.923</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- SCRIPT LOGIC -->
|
||||
<script>
|
||||
const sampleDb = {
|
||||
midi: [
|
||||
{ name: "MIDI_Loop_01.mid", events: 95, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130 },
|
||||
{ name: "MIDI_Loop_02_Bass.mid", events: 48, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130 },
|
||||
{ name: "MIDI_Loop_03_Lead.mid", events: 110, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130 },
|
||||
{ name: "MIDI_Loop_04.mid", events: 76, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130 },
|
||||
{ name: "MIDI_Loop_05_Bass.mid", events: 52, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130 },
|
||||
{ name: "MIDI_Loop_06.mid", events: 88, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130 }
|
||||
]
|
||||
};
|
||||
|
||||
let selectedFile = sampleDb.midi[3];
|
||||
const fileListTbody = document.getElementById('fileListTbody');
|
||||
const previewCanvas = document.getElementById('previewCanvas');
|
||||
const canvasCtx = previewCanvas.getContext('2d');
|
||||
|
||||
function renderFiles() {
|
||||
fileListTbody.innerHTML = '';
|
||||
sampleDb.midi.forEach((f) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = `cursor-pointer hover:bg-blue-100 ${f.name === selectedFile.name ? 'file-row-selected' : ''}`;
|
||||
tr.innerHTML = `<td class="py-1 px-2 flex items-center gap-2"><i class="fa-solid fa-music text-purple-600 file-icon"></i><span>${f.name}</span></td>`;
|
||||
tr.onclick = () => {
|
||||
selectedFile = f;
|
||||
renderFiles();
|
||||
updatePreview();
|
||||
};
|
||||
fileListTbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
document.getElementById('statusFileName').innerText = selectedFile.name;
|
||||
document.getElementById('metadataContent').innerHTML = `${selectedFile.events} MIDI events<br>Length: ${selectedFile.lengthQn} quarter notes<br>Length: ${selectedFile.time} (est)<br>Ticks per quarter note: ${selectedFile.tpqn}`;
|
||||
renderCanvas();
|
||||
}
|
||||
|
||||
function renderCanvas() {
|
||||
previewCanvas.width = previewCanvas.clientWidth;
|
||||
previewCanvas.height = previewCanvas.clientHeight;
|
||||
const w = previewCanvas.width, h = previewCanvas.height;
|
||||
canvasCtx.clearRect(0, 0, w, h);
|
||||
|
||||
// Grid
|
||||
canvasCtx.strokeStyle = '#222';
|
||||
for (let y = 0; y < h - 14; y += 8) {
|
||||
canvasCtx.beginPath(); canvasCtx.moveTo(0, y); canvasCtx.lineTo(w, y); canvasCtx.stroke();
|
||||
}
|
||||
canvasCtx.strokeStyle = '#333';
|
||||
for (let b = 0; b <= 16; b += 4) {
|
||||
const x = (b / 16) * w;
|
||||
canvasCtx.beginPath(); canvasCtx.moveTo(x, 0); canvasCtx.lineTo(x, h - 14); canvasCtx.stroke();
|
||||
}
|
||||
|
||||
// Notes
|
||||
canvasCtx.fillStyle = '#9ca3af';
|
||||
for (let i = 0; i < selectedFile.events; i++) {
|
||||
const noteX = ((i * 17 + 76) % 95) / 100 * w;
|
||||
const noteY = ((i * 13 + 76) % (h - 24)) + 4;
|
||||
canvasCtx.fillRect(noteX, noteY, Math.max(8, (i % 5 + 1) * 12), 3);
|
||||
}
|
||||
|
||||
// Bar Ruler
|
||||
canvasCtx.fillStyle = '#111';
|
||||
canvasCtx.fillRect(0, h - 14, w, 14);
|
||||
canvasCtx.fillStyle = '#888';
|
||||
canvasCtx.font = '9px JetBrains Mono';
|
||||
for (let b = 0; b <= 12; b += 4) {
|
||||
canvasCtx.fillText(b.toString(), (b / 16) * w + 2, h - 3);
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = () => { renderFiles(); updatePreview(); };
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
+3
-2
@@ -5,13 +5,14 @@ redis>=4.6.0
|
||||
python-multipart>=0.0.6
|
||||
librosa>=0.10.0
|
||||
pydub>=0.25.1
|
||||
numpy>=1.24.0
|
||||
numpy>=1.24.0,<2.0
|
||||
scipy>=1.10.0
|
||||
soundfile>=0.12.1
|
||||
jinja2>=3.1.2
|
||||
httpx>=0.24.0
|
||||
jsonschema>=4.18.0
|
||||
pedalboard>=0.8.0
|
||||
# pedalboard cài RIÊNG với --no-deps (tránh supertonic — kéo onnx2tf/opencv/pocket-tts
|
||||
# xung đột numpy<2 vs >=2 — dự án không dùng supertonic — 2026-08-08)
|
||||
mido>=1.3.0
|
||||
pyfluidsynth>=1.3.0
|
||||
sf2utils>=0.9.0
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "sonicforge-daw"
|
||||
version = "1.0.0"
|
||||
description = "Sonic Forge DAW - Desktop Studio"
|
||||
authors = ["SonicForge Studio"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "sonicforge_daw_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-shell = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = "s"
|
||||
strip = true
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capabilities for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"shell:allow-spawn",
|
||||
"shell:allow-open"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 957 B |
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
@@ -0,0 +1,40 @@
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use tauri_plugin_shell::process::CommandChild;
|
||||
use std::sync::Mutex;
|
||||
|
||||
struct EngineProcess(Mutex<Option<CommandChild>>);
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.setup(|app| {
|
||||
// 1. Spawn sidecar daw_engine.exe (background Python engine — 127.0.0.1:8000)
|
||||
let sidecar_command = app.shell().sidecar("daw_engine").unwrap();
|
||||
let (mut _rx, child) = sidecar_command.spawn().expect("Failed to spawn daw_engine sidecar");
|
||||
|
||||
app.manage(EngineProcess(Mutex::new(Some(child))));
|
||||
println!("Python Background Engine started successfully on localhost:8000");
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|window, event| {
|
||||
if let tauri::WindowEvent::Destroyed = event {
|
||||
// 2. Terminate sidecar khi window đóng — kill CẢ TREE (PyInstaller
|
||||
// onefile = parent + child — child.kill() chỉ giết parent → child
|
||||
// orphan chạy mãi — bug 09:30). taskkill /T giết cả nhánh.
|
||||
let state = window.state::<EngineProcess>();
|
||||
if let Ok(mut lock) = state.0.lock() {
|
||||
if let Some(child) = lock.take() {
|
||||
let _ = child.kill();
|
||||
}
|
||||
};
|
||||
let _ = std::process::Command::new("taskkill")
|
||||
.args(["/F", "/T", "/IM", "daw_engine.exe"])
|
||||
.output();
|
||||
println!("daw_engine sidecar terminated (tree killed).");
|
||||
}
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
sonicforge_daw_lib::run()
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "SonicForgeDAW",
|
||||
"version": "1.0.0",
|
||||
"identifier": "com.sonicforge.daw",
|
||||
"build": {
|
||||
"frontendDist": "../app/templates",
|
||||
"devUrl": "http://localhost:8000"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Sonic Forge DAW - Professional Desktop Studio",
|
||||
"url": "http://localhost:8000",
|
||||
"width": 1440,
|
||||
"height": 900,
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["nsis", "msi"],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"externalBin": [
|
||||
"binaries/daw_engine"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Pytest bootstrap: isolate the test suite from the development database.
|
||||
|
||||
Must be imported before any app module (pytest imports conftest.py first), so
|
||||
app.models.user picks up the test DB path instead of the dev DB. Without this,
|
||||
tests that seed/rotate the admin password (test_auth_and_quota) permanently
|
||||
mutate the developer's sonicforge.db.
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
os.environ.setdefault(
|
||||
"SONICFORGE_DB_PATH",
|
||||
os.path.join(tempfile.gettempdir(), "sonicforge_test.db"),
|
||||
)
|
||||
@@ -13,7 +13,17 @@ client = TestClient(app)
|
||||
def get_admin_token():
|
||||
resp = client.post("/api/v1/auth/login", json={"username": "admin", "password": "admin123"})
|
||||
if resp.status_code == 200:
|
||||
return resp.json()["access_token"]
|
||||
token = resp.json()["access_token"]
|
||||
# Admin is seeded with must_change_password=1; the app blocks music
|
||||
# processing until the first password change. Complete that flow here
|
||||
# (keeping the same password) so feature tests run unblocked.
|
||||
user = resp.json()["user"]
|
||||
if user.get("must_change_password"):
|
||||
r = client.post("/api/v1/auth/change-password", headers={"Authorization": f"Bearer {token}"},
|
||||
json={"old_password": "admin123", "new_password": "admin123"})
|
||||
if r.status_code == 200:
|
||||
token = r.json()["access_token"]
|
||||
return token
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Regression tests for legacy project upgrade (items must stay on their tracks
|
||||
with correct bar positions)."""
|
||||
import json
|
||||
|
||||
from app.api.v1.projects import upgrade_project_json_if_needed
|
||||
|
||||
|
||||
def _legacy_project():
|
||||
return {
|
||||
"id": "legacy_1",
|
||||
"name": "Legacy",
|
||||
"bpm": 120.0, # 1 bar = 2.0s
|
||||
"tracks": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Track 1",
|
||||
"volumeDb": 0.0,
|
||||
"pan": 0.0,
|
||||
"muted": False,
|
||||
"solo": False,
|
||||
"serverFileId": "abc.wav",
|
||||
"clips": [{"id": "c1", "name": "clip1", "startTime": 2.0}],
|
||||
"midiItems": [],
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "Track 2",
|
||||
"volumeDb": 0.0,
|
||||
"pan": 0.0,
|
||||
"muted": False,
|
||||
"solo": False,
|
||||
"serverFileId": None,
|
||||
"clips": [],
|
||||
"midiItems": [
|
||||
{"id": "m1", "name": "midi1", "startTime": 4.0, "duration": 4.0,
|
||||
"notes": [{"id": "n1", "pitch": 60, "start_beat": 0.0, "duration_beats": 1.0, "velocity": 0.8}]}
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_upgrade_keeps_items_on_their_tracks():
|
||||
upgraded = upgrade_project_json_if_needed(_legacy_project())
|
||||
tracks = upgraded["main_session"]["tracks"]
|
||||
assert len(tracks) == 2
|
||||
t1_items = tracks[0]["items"]
|
||||
t2_items = tracks[1]["items"]
|
||||
# Items must NOT be merged into the first track
|
||||
assert [i["type"] for i in t1_items] == ["AUDIO_ITEM"]
|
||||
assert [i["type"] for i in t2_items] == ["MIDI_ITEM"]
|
||||
assert t1_items[0]["id"] == "c1"
|
||||
assert t2_items[0]["id"] == "m1"
|
||||
|
||||
|
||||
def test_upgrade_uses_bpm_based_seconds_per_bar():
|
||||
upgraded = upgrade_project_json_if_needed(_legacy_project())
|
||||
tracks = upgraded["main_session"]["tracks"]
|
||||
# 120bpm -> 1 bar = 2.0s; clip at 2.0s -> start_bar 1.0
|
||||
assert tracks[0]["items"][0]["start_bar"] == 1.0
|
||||
# midi at 4.0s -> start_bar 2.0; duration 4.0s -> 2.0 bars
|
||||
assert tracks[1]["items"][0]["start_bar"] == 2.0
|
||||
assert tracks[1]["items"][0]["duration_bars"] == 2.0
|
||||
|
||||
|
||||
def test_upgrade_skips_new_format():
|
||||
data = {"main_session": {"tracks": []}}
|
||||
assert upgrade_project_json_if_needed(data) is data
|
||||
@@ -0,0 +1,234 @@
|
||||
"""Regression tests for security hardening.
|
||||
|
||||
Covers the vulnerabilities found during the 2026-08 audit:
|
||||
- SSRF / open proxy on /api/v1/ai/proxy
|
||||
- path traversal on render output and audio file ids
|
||||
- unauthenticated filesystem access via /api/v1/media/*
|
||||
- hardcoded SECRET_KEY
|
||||
- quota bypass on project update
|
||||
- audio resampling correctness in the render engine
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import soundfile as sf
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import app
|
||||
from app.config import settings
|
||||
from app.core import auth as core_auth
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def get_admin_token():
|
||||
# test_auth_and_quota.py may have rotated the admin password; try both.
|
||||
for pwd in ("admin123", "admin_new_password_2026"):
|
||||
resp = client.post("/api/v1/auth/login", json={"username": "admin", "password": pwd})
|
||||
if resp.status_code != 200:
|
||||
continue
|
||||
token = resp.json()["access_token"]
|
||||
user = resp.json()["user"]
|
||||
if user.get("must_change_password"):
|
||||
r = client.post("/api/v1/auth/change-password", headers={"Authorization": f"Bearer {token}"},
|
||||
json={"old_password": pwd, "new_password": pwd})
|
||||
if r.status_code == 200:
|
||||
token = r.json()["access_token"]
|
||||
return token
|
||||
return None
|
||||
|
||||
|
||||
def auth_headers():
|
||||
return {"Authorization": f"Bearer {get_admin_token()}"}
|
||||
|
||||
|
||||
# ── 1. SSRF / open proxy ──
|
||||
|
||||
class TestAIProxySSRF:
|
||||
def test_proxy_requires_auth(self):
|
||||
resp = client.post("/api/v1/ai/proxy", json={"url": "https://api.openai.com/v1", "body": {}})
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_proxy_blocks_cloud_metadata(self):
|
||||
resp = client.post("/api/v1/ai/proxy", headers=auth_headers(),
|
||||
json={"url": "http://169.254.169.254/latest/meta-data/", "body": {}})
|
||||
assert resp.status_code == 403
|
||||
|
||||
def test_proxy_blocks_private_ip_not_configured(self):
|
||||
resp = client.post("/api/v1/ai/proxy", headers=auth_headers(),
|
||||
json={"url": "http://10.0.0.5/", "body": {}})
|
||||
assert resp.status_code == 403
|
||||
|
||||
def test_proxy_rejects_non_http_scheme(self):
|
||||
resp = client.post("/api/v1/ai/proxy", headers=auth_headers(),
|
||||
json={"url": "file:///etc/passwd", "body": {}})
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_proxy_allows_configured_localhost_provider(self):
|
||||
# localhost:11434 is in the default AI provider list; it must pass the
|
||||
# SSRF check (and then fail to connect in this environment -> 502).
|
||||
resp = client.post("/api/v1/ai/proxy", headers=auth_headers(),
|
||||
json={"url": "http://localhost:11434/v1/chat/completions", "body": {}})
|
||||
assert resp.status_code == 502
|
||||
|
||||
|
||||
# ── 2. Path traversal ──
|
||||
|
||||
class TestPathTraversal:
|
||||
def test_render_output_filename_sanitized(self):
|
||||
token = get_admin_token()
|
||||
if not token:
|
||||
pytest.skip("Cannot get admin token")
|
||||
project = {
|
||||
"metadata": {"bpm": 120, "time_signature_numerator": 4},
|
||||
"main_session": {"length_bars": 1, "tracks": []},
|
||||
"section_store": {},
|
||||
}
|
||||
resp = client.post("/api/v1/plugins/render", headers=auth_headers(),
|
||||
json={"project_json": project, "output_filename": "/tmp/evil_traversal.wav"})
|
||||
# Absolute paths must be reduced to a basename inside PROCESSED_DIR.
|
||||
assert resp.status_code == 200, resp.text
|
||||
out_path = resp.json()["path"]
|
||||
assert os.path.dirname(out_path) == settings.PROCESSED_DIR
|
||||
assert os.path.basename(out_path) == "evil_traversal.wav"
|
||||
assert os.path.isfile(out_path)
|
||||
|
||||
def test_audio_download_rejects_traversal(self):
|
||||
resp = client.get("/api/v1/audio/download/..%2F..%2Fapp%2Fconfig.py")
|
||||
assert resp.status_code == 404
|
||||
|
||||
def test_ai_scan_rejects_traversal_file_id(self):
|
||||
resp = client.post("/api/v1/audio/ai-scan",
|
||||
json={"track_id": "1", "file_id": "../../app/config.py"})
|
||||
# Traversal must NOT read the file: falls through to the demo branch.
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["success"] is True
|
||||
|
||||
|
||||
# ── 3. Filesystem exposure via media endpoints ──
|
||||
|
||||
class TestMediaAuth:
|
||||
# Use a fresh client (no cookies from earlier logins) to prove 401.
|
||||
@pytest.fixture(autouse=True)
|
||||
def _fresh_client(self):
|
||||
self.fresh = TestClient(app)
|
||||
yield
|
||||
self.fresh.close()
|
||||
|
||||
def test_media_computer_requires_auth(self):
|
||||
resp = self.fresh.get("/api/v1/media/computer")
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_media_browse_requires_auth(self):
|
||||
resp = self.fresh.get("/api/v1/media/browse", params={"path": "/etc"})
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_media_file_requires_auth(self):
|
||||
resp = self.fresh.get("/api/v1/media/file", params={"path": "/etc/passwd"})
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
# ── 4. Secret key ──
|
||||
|
||||
class TestSecretKey:
|
||||
def test_secret_key_not_hardcoded_default(self):
|
||||
assert core_auth.SECRET_KEY != "sonicforge_secret_key_super_secure_2026"
|
||||
assert len(core_auth.SECRET_KEY) >= 32
|
||||
|
||||
|
||||
# ── 5. Quota enforcement on update ──
|
||||
|
||||
class TestQuotaUpdate:
|
||||
def test_update_cloud_project_enforces_quota(self):
|
||||
token = get_admin_token()
|
||||
if not token:
|
||||
pytest.skip("Cannot get admin token")
|
||||
# Register a fresh user with a small quota (unique name per run so the
|
||||
# test is re-runnable against a persistent DB).
|
||||
import uuid as _uuid
|
||||
uname = f"quota_user_{_uuid.uuid4().hex[:8]}"
|
||||
resp = client.post("/api/v1/auth/register", json={
|
||||
"username": uname, "email": f"{uname}@studio.com", "password": "quota_pass_123"})
|
||||
assert resp.status_code == 200, resp.text
|
||||
user_token = resp.json()["access_token"]
|
||||
user_headers = {"Authorization": f"Bearer {user_token}"}
|
||||
|
||||
# Shrink quota to 1 MB via admin API.
|
||||
uid = resp.json()["user"]["id"]
|
||||
r = client.put(f"/api/v1/admin/quotas/{uid}", headers=auth_headers(),
|
||||
json={"storage_limit_mb": 1, "max_tracks": 16})
|
||||
assert r.status_code == 200, r.text
|
||||
|
||||
# Save a small project.
|
||||
small = json.dumps({
|
||||
"project_id": "p1",
|
||||
"metadata": {"title": "small", "bpm": 120, "time_signature_numerator": 4,
|
||||
"time_signature_denominator": 4, "sample_rate": 44100},
|
||||
"main_session": {"id": "main", "name": "MAIN SESSION", "is_root": True,
|
||||
"length_bars": 16.0, "auto_compute_length": True, "tracks": []},
|
||||
"section_store": {}})
|
||||
r = client.post("/api/v1/projects/cloud", headers=user_headers,
|
||||
json={"name": "small", "data_json": small})
|
||||
assert r.status_code == 200, r.text
|
||||
pid = r.json()["project_id"]
|
||||
|
||||
# Updating with a payload over the quota must be rejected (was a bypass).
|
||||
items = [{
|
||||
"type": "AUDIO_ITEM", "id": f"it_{i}", "name": "n",
|
||||
"start_bar": 0.0, "duration_bars": 1.0, "clip_start_offset_bars": 0.0,
|
||||
"source_data": {"audio_file_url": "", "gain": 1.0},
|
||||
} for i in range(20000)]
|
||||
huge = json.dumps({
|
||||
"project_id": "p1",
|
||||
"metadata": {"title": "huge", "bpm": 120, "time_signature_numerator": 4,
|
||||
"time_signature_denominator": 4, "sample_rate": 44100},
|
||||
"main_session": {"id": "main", "name": "MAIN SESSION", "is_root": True,
|
||||
"length_bars": 16.0, "auto_compute_length": True,
|
||||
"tracks": [{"id": "t", "name": "x", "type": "AUDIO", "items": items}]},
|
||||
"section_store": {}})
|
||||
r = client.put(f"/api/v1/projects/cloud/{pid}", headers=user_headers,
|
||||
json={"name": "huge", "data_json": huge})
|
||||
assert r.status_code == 400, r.text
|
||||
assert "Quota" in r.json()["detail"]
|
||||
|
||||
|
||||
# ── 6. Render engine: resampling correctness ──
|
||||
|
||||
class TestRenderResample:
|
||||
def test_audio_item_resampled_to_engine_rate(self, tmp_path):
|
||||
from app.core.render_engine import PythonRenderEngine
|
||||
# 44.1kHz source, engine at 22.05kHz -> exactly 2x downsampling.
|
||||
sr_src = 44100
|
||||
t = np.arange(sr_src) / sr_src
|
||||
tone = (0.5 * np.sin(2 * np.pi * 440 * t)).astype(np.float32)
|
||||
src_path = os.path.join(settings.UPLOADS_DIR, "resample_test_tone.wav")
|
||||
sf.write(src_path, tone, sr_src)
|
||||
|
||||
engine = PythonRenderEngine(sample_rate=22050)
|
||||
session = {
|
||||
"tracks": [{
|
||||
"type": "AUDIO",
|
||||
"volume_db": 0.0, "pan": 0.0, "mute": False,
|
||||
"items": [{
|
||||
"type": "AUDIO_ITEM",
|
||||
"start_bar": 0.0, "duration_bars": 4.0,
|
||||
"clip_start_offset_bars": 0.0,
|
||||
"source_data": {"audio_file_url": "/static/audio/uploads/resample_test_tone.wav", "gain": 1.0},
|
||||
}],
|
||||
}]
|
||||
}
|
||||
buf = engine.render_session_container(session, {}, bpm=120.0, time_sig_num=4,
|
||||
total_samples=engine.sample_rate * 2)
|
||||
# A 1s 440Hz tone must actually render energy (previously the SR
|
||||
# mismatch silently skipped the audio).
|
||||
assert np.max(np.abs(buf)) > 0.01
|
||||
# Duration should be ~1 second at the engine rate, not 2.
|
||||
nonzero = np.where(np.abs(buf[0]) > 1e-4)[0]
|
||||
assert len(nonzero) > 0
|
||||
assert (nonzero[-1] - nonzero[0]) < int(engine.sample_rate * 1.3)
|
||||
try:
|
||||
os.remove(src_path)
|
||||
except OSError:
|
||||
pass
|
||||
@@ -0,0 +1,993 @@
|
||||
[
|
||||
{
|
||||
"id": "bca1bb5f-b656-48a8-b113-8fff900188ab",
|
||||
"name": "Test",
|
||||
"data_json": {
|
||||
"project_id": "project_1784710097790",
|
||||
"metadata": {
|
||||
"title": "Test",
|
||||
"bpm": 120.0,
|
||||
"time_signature_numerator": 4,
|
||||
"time_signature_denominator": 4,
|
||||
"sample_rate": 44100
|
||||
},
|
||||
"main_session": {
|
||||
"id": "main",
|
||||
"name": "MAIN SESSION",
|
||||
"is_root": true,
|
||||
"length_bars": 16.0,
|
||||
"auto_compute_length": true,
|
||||
"tracks": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Track 01",
|
||||
"type": "AUDIO",
|
||||
"volume_db": 0,
|
||||
"pan": 0,
|
||||
"mute": false,
|
||||
"solo": false,
|
||||
"fx_chain": [],
|
||||
"synth_engine": {
|
||||
"plugin_id": "synth",
|
||||
"preset_id": "default",
|
||||
"parameters": {}
|
||||
},
|
||||
"items": []
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "Track 02",
|
||||
"type": "AUDIO",
|
||||
"volume_db": 0,
|
||||
"pan": 0,
|
||||
"mute": false,
|
||||
"solo": false,
|
||||
"fx_chain": [],
|
||||
"synth_engine": {
|
||||
"plugin_id": "synth",
|
||||
"preset_id": "default",
|
||||
"parameters": {}
|
||||
},
|
||||
"items": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"section_store": {}
|
||||
},
|
||||
"updated_at": 1784710143.4476569
|
||||
},
|
||||
{
|
||||
"id": "d627f532-b632-4ef0-a21f-80dcc2ac8396",
|
||||
"name": "Test",
|
||||
"data_json": {
|
||||
"project_id": "project_1784719724178",
|
||||
"metadata": {
|
||||
"title": "Test",
|
||||
"bpm": 120.0,
|
||||
"time_signature_numerator": 4,
|
||||
"time_signature_denominator": 4,
|
||||
"sample_rate": 44100
|
||||
},
|
||||
"main_session": {
|
||||
"id": "main",
|
||||
"name": "MAIN SESSION",
|
||||
"is_root": true,
|
||||
"length_bars": 16.0,
|
||||
"auto_compute_length": true,
|
||||
"tracks": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Track 01",
|
||||
"type": "AUDIO",
|
||||
"volume_db": 0,
|
||||
"pan": 0,
|
||||
"mute": false,
|
||||
"solo": false,
|
||||
"fx_chain": [],
|
||||
"synth_engine": {
|
||||
"plugin_id": "synth",
|
||||
"preset_id": "default",
|
||||
"parameters": {}
|
||||
},
|
||||
"items": []
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "Track 02",
|
||||
"type": "AUDIO",
|
||||
"volume_db": 0,
|
||||
"pan": 0,
|
||||
"mute": false,
|
||||
"solo": false,
|
||||
"fx_chain": [],
|
||||
"synth_engine": {
|
||||
"plugin_id": "synth",
|
||||
"preset_id": "default",
|
||||
"parameters": {}
|
||||
},
|
||||
"items": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"section_store": {}
|
||||
},
|
||||
"updated_at": 1784719770.23144
|
||||
},
|
||||
{
|
||||
"id": "a17fe50b-c51d-408d-b45c-c09645777e68",
|
||||
"name": "Test",
|
||||
"data_json": {
|
||||
"project_id": "project_1784719775936",
|
||||
"metadata": {
|
||||
"title": "Test",
|
||||
"bpm": 120.0,
|
||||
"time_signature_numerator": 4,
|
||||
"time_signature_denominator": 4,
|
||||
"sample_rate": 44100
|
||||
},
|
||||
"main_session": {
|
||||
"id": "main",
|
||||
"name": "MAIN SESSION",
|
||||
"is_root": true,
|
||||
"length_bars": 16.0,
|
||||
"auto_compute_length": true,
|
||||
"tracks": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Track 01",
|
||||
"type": "AUDIO",
|
||||
"volume_db": 0,
|
||||
"pan": 0,
|
||||
"mute": false,
|
||||
"solo": false,
|
||||
"fx_chain": [],
|
||||
"synth_engine": {
|
||||
"plugin_id": "synth",
|
||||
"preset_id": "default",
|
||||
"parameters": {}
|
||||
},
|
||||
"items": []
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "Track 02",
|
||||
"type": "AUDIO",
|
||||
"volume_db": 0,
|
||||
"pan": 0,
|
||||
"mute": false,
|
||||
"solo": false,
|
||||
"fx_chain": [],
|
||||
"synth_engine": {
|
||||
"plugin_id": "synth",
|
||||
"preset_id": "default",
|
||||
"parameters": {}
|
||||
},
|
||||
"items": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"section_store": {}
|
||||
},
|
||||
"updated_at": 1784719821.9856758
|
||||
},
|
||||
{
|
||||
"id": "38a0f344-05dc-47e3-a769-07426c57f5fe",
|
||||
"name": "Test",
|
||||
"data_json": {
|
||||
"project_id": "project_1784720540893",
|
||||
"metadata": {
|
||||
"title": "Test",
|
||||
"bpm": 120.0,
|
||||
"time_signature_numerator": 4,
|
||||
"time_signature_denominator": 4,
|
||||
"sample_rate": 44100
|
||||
},
|
||||
"main_session": {
|
||||
"id": "main",
|
||||
"name": "MAIN SESSION",
|
||||
"is_root": true,
|
||||
"length_bars": 16.0,
|
||||
"auto_compute_length": true,
|
||||
"tracks": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Cartoon Capers Loop.mp3",
|
||||
"type": "AUDIO",
|
||||
"volume_db": 0,
|
||||
"pan": 0,
|
||||
"mute": false,
|
||||
"solo": false,
|
||||
"fx_chain": [],
|
||||
"synth_engine": {
|
||||
"plugin_id": "synth",
|
||||
"preset_id": "default",
|
||||
"parameters": {}
|
||||
},
|
||||
"items": []
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "Track 02",
|
||||
"type": "AUDIO",
|
||||
"volume_db": 0,
|
||||
"pan": 0,
|
||||
"mute": false,
|
||||
"solo": false,
|
||||
"fx_chain": [],
|
||||
"synth_engine": {
|
||||
"plugin_id": "synth",
|
||||
"preset_id": "default",
|
||||
"parameters": {}
|
||||
},
|
||||
"items": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"section_store": {}
|
||||
},
|
||||
"updated_at": 1784720586.9746954
|
||||
},
|
||||
{
|
||||
"id": "8037d8f5-1410-4639-8167-5b89dcb1c3e9",
|
||||
"name": "Rose",
|
||||
"data_json": {
|
||||
"project_id": "project_1784720555735",
|
||||
"metadata": {
|
||||
"title": "Rose",
|
||||
"bpm": 120.0,
|
||||
"time_signature_numerator": 4,
|
||||
"time_signature_denominator": 4,
|
||||
"sample_rate": 44100
|
||||
},
|
||||
"main_session": {
|
||||
"id": "main",
|
||||
"name": "MAIN SESSION",
|
||||
"is_root": true,
|
||||
"length_bars": 16.0,
|
||||
"auto_compute_length": true,
|
||||
"tracks": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Track 01",
|
||||
"type": "AUDIO",
|
||||
"volume_db": 0,
|
||||
"pan": 0,
|
||||
"mute": false,
|
||||
"solo": false,
|
||||
"fx_chain": [],
|
||||
"synth_engine": {
|
||||
"plugin_id": "synth",
|
||||
"preset_id": "default",
|
||||
"parameters": {}
|
||||
},
|
||||
"items": []
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "Track 02",
|
||||
"type": "AUDIO",
|
||||
"volume_db": 0,
|
||||
"pan": 0,
|
||||
"mute": false,
|
||||
"solo": false,
|
||||
"fx_chain": [],
|
||||
"synth_engine": {
|
||||
"plugin_id": "synth",
|
||||
"preset_id": "default",
|
||||
"parameters": {}
|
||||
},
|
||||
"items": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"section_store": {}
|
||||
},
|
||||
"updated_at": 1784720601.8155787
|
||||
},
|
||||
{
|
||||
"id": "525bdab5-e594-4a0c-a3e6-102a0abef816",
|
||||
"name": "Rose (autosave 03/08)",
|
||||
"data_json": {
|
||||
"project_id": "8037d8f5-1410-4639-8167-5b89dcb1c3e9",
|
||||
"metadata": {
|
||||
"title": "Rose (autosave 03/08)",
|
||||
"bpm": 128,
|
||||
"time_signature_numerator": 4,
|
||||
"time_signature_denominator": 4,
|
||||
"sample_rate": 44100
|
||||
},
|
||||
"main_session": {
|
||||
"id": "main",
|
||||
"name": "MAIN SESSION",
|
||||
"is_root": true,
|
||||
"length_bars": 16,
|
||||
"auto_compute_length": true,
|
||||
"tracks": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Track 01",
|
||||
"type": "MIDI",
|
||||
"volume_db": 0,
|
||||
"pan": 0,
|
||||
"mute": false,
|
||||
"solo": false,
|
||||
"instrument_id": "sf_DSK_Asian_DreamZ",
|
||||
"instrument_program": 5,
|
||||
"instrument_name": "BAN-DI",
|
||||
"items": [
|
||||
{
|
||||
"id": "midi_1785057918746",
|
||||
"name": "MIDI Item",
|
||||
"type": "MIDI_ITEM",
|
||||
"start_bar": 0,
|
||||
"duration_bars": 4,
|
||||
"clip_start_offset_bars": 0,
|
||||
"source_data": {
|
||||
"total_buffer_bars": 4,
|
||||
"notes": [
|
||||
{
|
||||
"id": "note_1785057946413j2a54",
|
||||
"pitch": 55,
|
||||
"start_beat": 1.5,
|
||||
"duration_beats": 0.125,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946488uv1f6r6q_2",
|
||||
"pitch": 56,
|
||||
"start_beat": 1.5166666666666666,
|
||||
"duration_beats": 0.2333333333333334,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946505gxl5rre2_3",
|
||||
"pitch": 57,
|
||||
"start_beat": 1.75,
|
||||
"duration_beats": 0.3833333333333333,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946522v0dmipvj_4",
|
||||
"pitch": 60,
|
||||
"start_beat": 2.1333333333333333,
|
||||
"duration_beats": 0.2666666666666666,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_17850579465381vqpeamx_5",
|
||||
"pitch": 62,
|
||||
"start_beat": 2.4,
|
||||
"duration_beats": 0.21666666666666679,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946555rmpgksfp_6",
|
||||
"pitch": 63,
|
||||
"start_beat": 2.6166666666666667,
|
||||
"duration_beats": 0.20000000000000018,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946572cvrxri09_7",
|
||||
"pitch": 64,
|
||||
"start_beat": 2.816666666666667,
|
||||
"duration_beats": 0.25,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946588nvmp7zld_8",
|
||||
"pitch": 66,
|
||||
"start_beat": 3.066666666666667,
|
||||
"duration_beats": 0.1499999999999999,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946605jkbnh8mf_9",
|
||||
"pitch": 67,
|
||||
"start_beat": 3.216666666666667,
|
||||
"duration_beats": 0.2666666666666666,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946638i28svktt_10",
|
||||
"pitch": 68,
|
||||
"start_beat": 3.4833333333333334,
|
||||
"duration_beats": 0.1499999999999999,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_17850579466559iy45ml8_11",
|
||||
"pitch": 69,
|
||||
"start_beat": 3.6333333333333333,
|
||||
"duration_beats": 0.1333333333333333,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946672s712j5m6_12",
|
||||
"pitch": 70,
|
||||
"start_beat": 3.7666666666666666,
|
||||
"duration_beats": 0.1333333333333333,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946688pq858zcx_13",
|
||||
"pitch": 71,
|
||||
"start_beat": 3.9,
|
||||
"duration_beats": 0.18333333333333313,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_17850579467054usc79tx_14",
|
||||
"pitch": 72,
|
||||
"start_beat": 4.083333333333333,
|
||||
"duration_beats": 0.15000000000000036,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946722dspmlfaa_15",
|
||||
"pitch": 73,
|
||||
"start_beat": 4.233333333333333,
|
||||
"duration_beats": 0.2999999999999998,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946755h5mvip3h_16",
|
||||
"pitch": 74,
|
||||
"start_beat": 4.533333333333333,
|
||||
"duration_beats": 0.15000000000000036,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946772a5e5iona_17",
|
||||
"pitch": 75,
|
||||
"start_beat": 4.683333333333334,
|
||||
"duration_beats": 0.16666666666666607,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946788cfqxk2c8_18",
|
||||
"pitch": 76,
|
||||
"start_beat": 4.85,
|
||||
"duration_beats": 0.2666666666666666,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_178505794682247xy477z_19",
|
||||
"pitch": 77,
|
||||
"start_beat": 5.116666666666666,
|
||||
"duration_beats": 0.25,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946855nygukqis_20",
|
||||
"pitch": 78,
|
||||
"start_beat": 5.366666666666666,
|
||||
"duration_beats": 0.20000000000000018,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946888oyfvmvq2_21",
|
||||
"pitch": 79,
|
||||
"start_beat": 5.566666666666666,
|
||||
"duration_beats": 0.35000000000000053,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947455xrq7rfmm_22",
|
||||
"pitch": 78,
|
||||
"start_beat": 5.916666666666667,
|
||||
"duration_beats": 0.2833333333333332,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_17850579474722sn8bsed_23",
|
||||
"pitch": 77,
|
||||
"start_beat": 6.2,
|
||||
"duration_beats": 0.31666666666666643,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947489mbv5z7q6_24",
|
||||
"pitch": 76,
|
||||
"start_beat": 6.516666666666667,
|
||||
"duration_beats": 0.2833333333333332,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947505qmasllso_25",
|
||||
"pitch": 75,
|
||||
"start_beat": 6.8,
|
||||
"duration_beats": 0.41666666666666696,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947539wkqyr6fw_26",
|
||||
"pitch": 74,
|
||||
"start_beat": 7.216666666666667,
|
||||
"duration_beats": 0.18333333333333357,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947572a2ydjwzu_27",
|
||||
"pitch": 73,
|
||||
"start_beat": 7.4,
|
||||
"duration_beats": 0.1999999999999993,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947589b18pcser_28",
|
||||
"pitch": 72,
|
||||
"start_beat": 7.6,
|
||||
"duration_beats": 0.16666666666666696,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947605bsiv751r_29",
|
||||
"pitch": 71,
|
||||
"start_beat": 7.766666666666667,
|
||||
"duration_beats": 0.13333333333333375,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947622ii453i6x_30",
|
||||
"pitch": 70,
|
||||
"start_beat": 7.9,
|
||||
"duration_beats": 0.18333333333333357,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947639zdv6seea_31",
|
||||
"pitch": 69,
|
||||
"start_beat": 8.083333333333334,
|
||||
"duration_beats": 0.36666666666666536,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947672devle9p7_32",
|
||||
"pitch": 68,
|
||||
"start_beat": 8.45,
|
||||
"duration_beats": 0.25,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947689x8rgn7ih_33",
|
||||
"pitch": 67,
|
||||
"start_beat": 8.7,
|
||||
"duration_beats": 0.3333333333333339,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947722u5b36hrl_34",
|
||||
"pitch": 66,
|
||||
"start_beat": 9.033333333333333,
|
||||
"duration_beats": 0.2833333333333332,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947772f9qp4st0_35",
|
||||
"pitch": 65,
|
||||
"start_beat": 9.316666666666666,
|
||||
"duration_beats": 0.38333333333333286,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947805cf5bjiv1_36",
|
||||
"pitch": 64,
|
||||
"start_beat": 9.7,
|
||||
"duration_beats": 0.6666666666666679,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947855ojfcoccj_37",
|
||||
"pitch": 63,
|
||||
"start_beat": 10.366666666666667,
|
||||
"duration_beats": 0.36666666666666536,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_17850579478721jvhzolj_38",
|
||||
"pitch": 62,
|
||||
"start_beat": 10.733333333333333,
|
||||
"duration_beats": 0.3000000000000007,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947889nqk9ckue_39",
|
||||
"pitch": 61,
|
||||
"start_beat": 11.033333333333333,
|
||||
"duration_beats": 0.3333333333333339,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "Track 02",
|
||||
"type": "AUDIO",
|
||||
"volume_db": 0,
|
||||
"pan": 0,
|
||||
"mute": false,
|
||||
"solo": false,
|
||||
"instrument_id": null,
|
||||
"instrument_program": null,
|
||||
"instrument_name": null,
|
||||
"items": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"sub_tabs": [
|
||||
{
|
||||
"id": "midi_1785057919362",
|
||||
"label": "Piano Roll: MIDI Item",
|
||||
"type": "PIANO_ROLL",
|
||||
"track_id": "1",
|
||||
"target_id": "midi_1785057918746",
|
||||
"parent_tab_id": null,
|
||||
"notes": [
|
||||
{
|
||||
"id": "note_1785057946413j2a54",
|
||||
"pitch": 55,
|
||||
"start_beat": 1.5,
|
||||
"duration_beats": 0.125,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946488uv1f6r6q_2",
|
||||
"pitch": 56,
|
||||
"start_beat": 1.5166666666666666,
|
||||
"duration_beats": 0.2333333333333334,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946505gxl5rre2_3",
|
||||
"pitch": 57,
|
||||
"start_beat": 1.75,
|
||||
"duration_beats": 0.3833333333333333,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946522v0dmipvj_4",
|
||||
"pitch": 60,
|
||||
"start_beat": 2.1333333333333333,
|
||||
"duration_beats": 0.2666666666666666,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_17850579465381vqpeamx_5",
|
||||
"pitch": 62,
|
||||
"start_beat": 2.4,
|
||||
"duration_beats": 0.21666666666666679,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946555rmpgksfp_6",
|
||||
"pitch": 63,
|
||||
"start_beat": 2.6166666666666667,
|
||||
"duration_beats": 0.20000000000000018,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946572cvrxri09_7",
|
||||
"pitch": 64,
|
||||
"start_beat": 2.816666666666667,
|
||||
"duration_beats": 0.25,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946588nvmp7zld_8",
|
||||
"pitch": 66,
|
||||
"start_beat": 3.066666666666667,
|
||||
"duration_beats": 0.1499999999999999,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946605jkbnh8mf_9",
|
||||
"pitch": 67,
|
||||
"start_beat": 3.216666666666667,
|
||||
"duration_beats": 0.2666666666666666,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946638i28svktt_10",
|
||||
"pitch": 68,
|
||||
"start_beat": 3.4833333333333334,
|
||||
"duration_beats": 0.1499999999999999,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_17850579466559iy45ml8_11",
|
||||
"pitch": 69,
|
||||
"start_beat": 3.6333333333333333,
|
||||
"duration_beats": 0.1333333333333333,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946672s712j5m6_12",
|
||||
"pitch": 70,
|
||||
"start_beat": 3.7666666666666666,
|
||||
"duration_beats": 0.1333333333333333,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946688pq858zcx_13",
|
||||
"pitch": 71,
|
||||
"start_beat": 3.9,
|
||||
"duration_beats": 0.18333333333333313,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_17850579467054usc79tx_14",
|
||||
"pitch": 72,
|
||||
"start_beat": 4.083333333333333,
|
||||
"duration_beats": 0.15000000000000036,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946722dspmlfaa_15",
|
||||
"pitch": 73,
|
||||
"start_beat": 4.233333333333333,
|
||||
"duration_beats": 0.2999999999999998,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946755h5mvip3h_16",
|
||||
"pitch": 74,
|
||||
"start_beat": 4.533333333333333,
|
||||
"duration_beats": 0.15000000000000036,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946772a5e5iona_17",
|
||||
"pitch": 75,
|
||||
"start_beat": 4.683333333333334,
|
||||
"duration_beats": 0.16666666666666607,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946788cfqxk2c8_18",
|
||||
"pitch": 76,
|
||||
"start_beat": 4.85,
|
||||
"duration_beats": 0.2666666666666666,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_178505794682247xy477z_19",
|
||||
"pitch": 77,
|
||||
"start_beat": 5.116666666666666,
|
||||
"duration_beats": 0.25,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946855nygukqis_20",
|
||||
"pitch": 78,
|
||||
"start_beat": 5.366666666666666,
|
||||
"duration_beats": 0.20000000000000018,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057946888oyfvmvq2_21",
|
||||
"pitch": 79,
|
||||
"start_beat": 5.566666666666666,
|
||||
"duration_beats": 0.35000000000000053,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947455xrq7rfmm_22",
|
||||
"pitch": 78,
|
||||
"start_beat": 5.916666666666667,
|
||||
"duration_beats": 0.2833333333333332,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_17850579474722sn8bsed_23",
|
||||
"pitch": 77,
|
||||
"start_beat": 6.2,
|
||||
"duration_beats": 0.31666666666666643,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947489mbv5z7q6_24",
|
||||
"pitch": 76,
|
||||
"start_beat": 6.516666666666667,
|
||||
"duration_beats": 0.2833333333333332,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947505qmasllso_25",
|
||||
"pitch": 75,
|
||||
"start_beat": 6.8,
|
||||
"duration_beats": 0.41666666666666696,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947539wkqyr6fw_26",
|
||||
"pitch": 74,
|
||||
"start_beat": 7.216666666666667,
|
||||
"duration_beats": 0.18333333333333357,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947572a2ydjwzu_27",
|
||||
"pitch": 73,
|
||||
"start_beat": 7.4,
|
||||
"duration_beats": 0.1999999999999993,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947589b18pcser_28",
|
||||
"pitch": 72,
|
||||
"start_beat": 7.6,
|
||||
"duration_beats": 0.16666666666666696,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947605bsiv751r_29",
|
||||
"pitch": 71,
|
||||
"start_beat": 7.766666666666667,
|
||||
"duration_beats": 0.13333333333333375,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947622ii453i6x_30",
|
||||
"pitch": 70,
|
||||
"start_beat": 7.9,
|
||||
"duration_beats": 0.18333333333333357,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947639zdv6seea_31",
|
||||
"pitch": 69,
|
||||
"start_beat": 8.083333333333334,
|
||||
"duration_beats": 0.36666666666666536,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947672devle9p7_32",
|
||||
"pitch": 68,
|
||||
"start_beat": 8.45,
|
||||
"duration_beats": 0.25,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947689x8rgn7ih_33",
|
||||
"pitch": 67,
|
||||
"start_beat": 8.7,
|
||||
"duration_beats": 0.3333333333333339,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947722u5b36hrl_34",
|
||||
"pitch": 66,
|
||||
"start_beat": 9.033333333333333,
|
||||
"duration_beats": 0.2833333333333332,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947772f9qp4st0_35",
|
||||
"pitch": 65,
|
||||
"start_beat": 9.316666666666666,
|
||||
"duration_beats": 0.38333333333333286,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947805cf5bjiv1_36",
|
||||
"pitch": 64,
|
||||
"start_beat": 9.7,
|
||||
"duration_beats": 0.6666666666666679,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947855ojfcoccj_37",
|
||||
"pitch": 63,
|
||||
"start_beat": 10.366666666666667,
|
||||
"duration_beats": 0.36666666666666536,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_17850579478721jvhzolj_38",
|
||||
"pitch": 62,
|
||||
"start_beat": 10.733333333333333,
|
||||
"duration_beats": 0.3000000000000007,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
},
|
||||
{
|
||||
"id": "note_1785057947889nqk9ckue_39",
|
||||
"pitch": 61,
|
||||
"start_beat": 11.033333333333333,
|
||||
"duration_beats": 0.3333333333333339,
|
||||
"velocity": 0.8,
|
||||
"pan": 0
|
||||
}
|
||||
],
|
||||
"duration": 7.5,
|
||||
"instrument_program": 5,
|
||||
"instrument_name": "BAN-DI",
|
||||
"current_time": 0,
|
||||
"color": null
|
||||
}
|
||||
],
|
||||
"section_store": {}
|
||||
},
|
||||
"updated_at": 1785058087.45
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
import struct, zlib, os
|
||||
|
||||
def make_png(path, size):
|
||||
w = h = size
|
||||
raw = b''
|
||||
for y in range(h):
|
||||
row = b'\x00'
|
||||
for x in range(w):
|
||||
cx, cy = x - w / 2, y - h / 2
|
||||
d = (cx * cx + cy * cy) ** 0.5 / (w / 2)
|
||||
r = int(120 + 60 * (1 - d))
|
||||
g = int(60 + 40 * (1 - d))
|
||||
b = int(200 + 40 * (1 - d))
|
||||
row += bytes((r, g, b, 255))
|
||||
raw += row
|
||||
def chunk(tag, data):
|
||||
c = tag + data
|
||||
return struct.pack('>I', len(data)) + c + struct.pack('>I', zlib.crc32(c) % 0x100000000)
|
||||
png = b'\x89PNG\r\n\x1a\n'
|
||||
png += chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 6, 0, 0, 0))
|
||||
png += chunk(b'IDAT', zlib.compress(raw, 9))
|
||||
png += chunk(b'IEND', b'')
|
||||
open(path, 'wb').write(png)
|
||||
|
||||
os.makedirs('src-tauri/icons', exist_ok=True)
|
||||
make_png('src-tauri/icons/32x32.png', 32)
|
||||
make_png('src-tauri/icons/128x128.png', 128)
|
||||
png128 = open('src-tauri/icons/128x128.png', 'rb').read()
|
||||
ico = struct.pack('<HHH', 0, 1, 1) + struct.pack('<BBBBHHII', 128, 128, 0, 0, 1, 32, len(png128), 22) + png128
|
||||
open('src-tauri/icons/icon.ico', 'wb').write(ico)
|
||||
open('src-tauri/icons/icon.png', 'wb').write(png128)
|
||||
print('icons:', os.listdir('src-tauri/icons'))
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Khôi phục các dự án cloud cũ (từ git history) vào bất kỳ sonicforge.db nào.
|
||||
|
||||
Cách dùng trên máy deployment thật (host game):
|
||||
python3 tools/restore_cloud_projects.py /path/to/sonicforge.db
|
||||
|
||||
Script đọc backup projects (JSON) đã xuất từ git history và chèn vào DB chỉ định,
|
||||
gán tất cả cho user_id được yêu cầu (mặc định: admin đầu tiên tìm thấy).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
PROJECTS_BACKUP = os.path.join(os.path.dirname(__file__), "cloud_projects_backup.json")
|
||||
|
||||
|
||||
def main(db_path: str, user_id: str = None):
|
||||
if not os.path.exists(PROJECTS_BACKUP):
|
||||
print(f"Không tìm thấy {PROJECTS_BACKUP}")
|
||||
return 1
|
||||
if not os.path.exists(db_path):
|
||||
print(f"Không tìm thấy DB: {db_path}")
|
||||
return 1
|
||||
|
||||
with open(PROJECTS_BACKUP) as f:
|
||||
projects = json.load(f)
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
if user_id is None:
|
||||
row = conn.execute("SELECT id FROM users WHERE role='admin' ORDER BY created_at LIMIT 1").fetchone()
|
||||
if not row:
|
||||
print("Không có user admin nào trong DB")
|
||||
conn.close()
|
||||
return 1
|
||||
user_id = row["id"]
|
||||
print(f"Gán tất cả cho admin: {user_id}")
|
||||
|
||||
restored = 0
|
||||
for p in projects:
|
||||
if conn.execute("SELECT id FROM projects WHERE id=?", (p["id"],)).fetchone():
|
||||
print(f" bỏ qua (đã tồn tại): {p['name']}")
|
||||
continue
|
||||
conn.execute(
|
||||
"INSERT INTO projects (id, user_id, name, data_json, is_temp, size_bytes, updated_at) VALUES (?,?,?,?,0,?,?)",
|
||||
(p["id"], user_id, p["name"], json.dumps(p["data_json"], ensure_ascii=False),
|
||||
len(json.dumps(p["data_json"]).encode("utf-8")), p["updated_at"]))
|
||||
restored += 1
|
||||
print(f" đã khôi phục: {p['name']}")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print(f"\nHoàn tất: {restored} dự án đã khôi phục cho user {user_id}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 restore_cloud_projects.py <path/to/sonicforge.db> [user_id]")
|
||||
sys.exit(1)
|
||||
sys.exit(main(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else None))
|
||||
Reference in New Issue
Block a user