From 322748898b6897754d869fe63a816dce926e1314 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Mon, 12 Oct 2026 21:03:17 +0700 Subject: [PATCH] P0: cat torch/torchvision khoi engine bundle (1842MB -> 147MB) + hop dong API + golden conformance + baseline --- engine.spec | 7 + md/BASELINE_P0.md | 46 + md/RUST_PORT_PLAN.md | 14 +- md/api_contract/golden/manifest.json | 22 + md/api_contract/openapi.json | 7233 ++++++++++++++++++++++++++ md/api_contract/ws_protocol.md | 72 + tools/golden_conformance.py | 193 + tools/p0_baseline.py | 168 + 8 files changed, 7749 insertions(+), 6 deletions(-) create mode 100644 md/BASELINE_P0.md create mode 100644 md/api_contract/golden/manifest.json create mode 100644 md/api_contract/openapi.json create mode 100644 md/api_contract/ws_protocol.md create mode 100644 tools/golden_conformance.py create mode 100644 tools/p0_baseline.py diff --git a/engine.spec b/engine.spec index 0f8c36b..c7750a6 100644 --- a/engine.spec +++ b/engine.spec @@ -137,6 +137,13 @@ _excludes = [ 'matplotlib', 'pandas', 'IPython', 'jupyter', 'pytest', 'setuptools', # keo vao nham boi hooks_contrib (app khong import bao gio) 'PIL', 'Pillow', 'cairosvg', 'zstandard', 'imageio', + # P0 (2026-09): torch/torchvision (~1.5GB) bi keo vao boi + # scipy._lib.array_api_compat.common._helpers ('import torch' trong try/except). + # App KHONG import torch o bat ky dau - da kiem chung: block import torch -> + # scipy.signal + moi module app.core import van OK. Loai ca cay AI nang. + 'torch', 'torchvision', 'torchaudio', 'triton', + 'sympy', 'networkx', 'tqdm', 'einops', 'optree', + 'tensorflow', 'keras', 'jax', 'jaxlib', ] a = Analysis( diff --git a/md/BASELINE_P0.md b/md/BASELINE_P0.md new file mode 100644 index 0000000..5a75b68 --- /dev/null +++ b/md/BASELINE_P0.md @@ -0,0 +1,46 @@ +# Baseline P0 - SonicForgeStudio (Python engine) + +Sinh boi `tools/p0_baseline.py` ngay 2026-10-12 21:00. Dung lam moc so sanh khi port Rust. + +```json +{ + "date": "2026-10-12 21:00", + "bundle": { + "dist/daw_engine": { + "mb": 146.6, + "files": 403 + }, + "src-tauri/resources/daw_engine": { + "mb": 1841.8, + "files": 4187 + }, + "has:torch": false, + "has:torchvision": false, + "has:sympy": false, + "has:networkx": false, + "has:tensorflow": false + }, + "cold_start": { + "startup_ms": 5700.4, + "port": 8000, + "rss_mb": 68.3 + }, + "import": { + "import_app_main_ms": 730.9, + "subprocess_wall_ms": 822.0 + }, + "render_mixdown": { + "tracks": 4, + "seconds_each": 30.0, + "sr": 48000, + "runs_ms": [ + 1808.2, + 515.9, + 529.0 + ], + "min_ms": 515.9, + "median_ms": 529.0, + "realtime_factor": 0.0172 + } +} +``` diff --git a/md/RUST_PORT_PLAN.md b/md/RUST_PORT_PLAN.md index eb07a51..318318b 100644 --- a/md/RUST_PORT_PLAN.md +++ b/md/RUST_PORT_PLAN.md @@ -128,7 +128,7 @@ Chia hệ thống thành 5 tầng, mỗi tầng có "hệ số port" khác nhau: ### 4.1 Việc tối ưu "miễn phí", làm được NGAY (không cần port) -1. **Xoá `torch` + `torchvision` khỏi bundle** (không được import ở đâu trong source) → −1,5 GB, giảm thời gian build/install ngay lập tức. Chỉ cần loại khỏi môi trường build / `engine.spec` và kiểm tra lại bằng test suite. +1. ✅ **Đã làm (P0)**: xoá `torch` + `torchvision` khỏi bundle → **1.842 MB → 146,6 MB**, không cần port. Chỉ cần thêm `excludes` trong `engine.spec`; đã kiểm chứng app không import torch (`tools/p0_baseline.py`, `md/BASELINE_P0.md`). 2. Vá `app/static/js/app.jsx` 37k dòng bằng bundler thật (esbuild/vite) thay vì Babel 1-file → `app.precompiled.js` nhỏ hơn, build nhanh hơn, có source map. 3. Tách `app.jsx` thành module (đang là 1 component khổng lồ) → giảm rủi ro regression, tiền đề bắt buộc nếu sau này port UI. 4. Thay relay `WebView → WS → Python → SHM → C++` bằng **Tauri IPC trực tiếp tới Rust → SHM** cho phần control: bỏ 1 hop mạng loopback, bỏ khả năng treo do uvicorn, giảm jitter điều khiển. Đây là bước rẻ nhất trong hướng T4. @@ -170,11 +170,13 @@ Chia hệ thống thành 5 tầng, mỗi tầng có "hệ số port" khác nhau: ## 5. Kế hoạch triển khai theo giai đoạn -### P0 — Dọn dẹp trước khi port (1-3 ngày, không rủi ro) -- [ ] Xác nhận `torch`/`torchvision` không dùng → loại khỏi build, đo lại bundle (mục tiêu <400 MB ngay). -- [ ] Chốt "hợp đồng API": export OpenAPI JSON + danh sách message WS (`set_chain`, `set_param`, `flush`, `started`, `stats`…) làm đặc tả cho bản Rust. -- [ ] Dựng **golden test**: bộ WAV/JSON mẫu cho từng endpoint DSP; chạy trên Python hiện tại, lưu hash → đây là tiêu chuẩn đúng cho Rust. -- [ ] Đo baseline: thời gian start, RAM, thời gian scan plugin, thời gian render (ghi vào `md/`). +### P0 — Dọn dẹp trước khi port — ✅ ĐÃ XONG (2026-10-12) +- [x] Xác nhận `torch`/`torchvision` không dùng (block import torch → `scipy.signal` + mọi `app.core.*` import OK) → thêm exclude vào `engine.spec` (torch/torchvision/torchaudio/triton/sympy/networkx/tqdm/einops/optree/tensorflow/keras/jax). + **Kết quả: bundle engine 1.841,8 MB / 4.187 file → 146,6 MB / 403 file (−92%)**, đã thay cả bản ship trong `src-tauri/resources/daw_engine`. +- [x] Chốt "hợp đồng API": `md/api_contract/openapi.json` (86 path / 95 operation / 53 schema) + `md/api_contract/ws_protocol.md` (WS realtime + worklet). +- [x] Golden test: `tools/golden_conformance.py` → `md/api_contract/golden/manifest.json`, 10 case DSP (zero-crossing, micro-crossfade, peak/RMS waveform, cut/loop, multitrack mixdown, analyze, LUFS/true-peak/mono-side). Tái lập ổn định (`--check` = 0 mismatch giữa các tiến trình); mixdown hash theo mẫu audio vì header WAV không ổn định. +- [x] Đo baseline: `tools/p0_baseline.py` → `md/BASELINE_P0.md` (cold start **5.700 ms**, RSS **68 MB**, import `app.main` 731 ms, mixdown 4 track × 30 s: min **516 ms** = 0,017× realtime). + ### P1 — Rust service khung, chạy song song (2-4 tuần) - [ ] Crate `engine/` (axum): router, auth, health, `/api/v1/system`, `/tasks`. diff --git a/md/api_contract/golden/manifest.json b/md/api_contract/golden/manifest.json new file mode 100644 index 0000000..a80ab46 --- /dev/null +++ b/md/api_contract/golden/manifest.json @@ -0,0 +1,22 @@ +{ + "cases": { + "analyzer.analyze_audio": "20fb044c076bc180810a92edb0cf247f62b2dea5e16f769208b634deeb6f36ef", + "audio_editor.cut_and_loop_segment": "16848af659a11994904d4d08d3b91ba58224ce0f057cc950b645c0c3a8417226", + "audio_editor.mix_multitrack_session": "46dcd4da24a0765cc78707eb4079fc289cc1e23bc741ecd12309683c59ce15bc", + "dsp_utils.apply_micro_crossfade": "c43756937a0e5a2e285c9af1d8d848041033fbb3396057d8ca3bfc513bdca758", + "dsp_utils.find_zero_crossing": "8ee6913ca5066eb6b4949040e6cb0f3d47589802de67b886d905b2ea79049dba", + "dsp_utils.generate_peak_waveform": "1a629e10dd4207bb8faafee747eac4955b05d3e624236e0a13bb51bd0f2a5f82", + "dsp_utils.generate_rms_waveform": "6ecf9662cf77a6a3e8b193dfef9b32d32bdff14c355a8915cd8f81ee44b80f5b", + "loudness.integrated_lufs": "0914a33306e2832bba22f7bfed82a8c30c785752b3a55dab3bb5151354709972", + "loudness.mono_side_ratio": "6ab9135f88cfd6092ccd2c3f0eb3285debd7227f9675b7e8a6cfe86b473b3c0c", + "loudness.true_peak_db": "4726ed91ff71f4c2fd9f66bef790916cb7574bd715598af0ff146fa582520e56" + }, + "env": { + "numpy": "1.26.4", + "python": "3.13.7", + "sample_rate": 48000, + "scipy": "1.17.1" + }, + "generated_by": "tools/golden_conformance.py", + "schema": 1 +} \ No newline at end of file diff --git a/md/api_contract/openapi.json b/md/api_contract/openapi.json new file mode 100644 index 0000000..b7256e7 --- /dev/null +++ b/md/api_contract/openapi.json @@ -0,0 +1,7233 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "ScoreFX API Engine", + "version": "0.1.0" + }, + "paths": { + "/api/v1/audio/upload": { + "post": { + "tags": [ + "audio" + ], + "summary": "Upload Audio", + "operationId": "upload_audio_api_v1_audio_upload_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_upload_audio_api_v1_audio_upload_post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/audio/edit": { + "post": { + "tags": [ + "audio" + ], + "summary": "Edit Audio", + "operationId": "edit_audio_api_v1_audio_edit_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EditRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/audio/download/{file_id}": { + "get": { + "tags": [ + "audio" + ], + "summary": "Download Audio", + "operationId": "download_audio_api_v1_audio_download__file_id__get", + "parameters": [ + { + "name": "file_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "File Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/audio/waveform/{file_id}": { + "get": { + "tags": [ + "audio" + ], + "summary": "Get Waveform", + "description": "API endpoint vẽ Peak Waveform đồng bộ (Week 2).\nTrả về dữ liệu peak waveform cho hiển thị đồ thị sóng âm trên Frontend.", + "operationId": "get_waveform_api_v1_audio_waveform__file_id__get", + "parameters": [ + { + "name": "file_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "File Id" + } + }, + { + "name": "num_peaks", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 4000, + "minimum": 50, + "default": 800, + "title": "Num Peaks" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/audio/waveform-rms/{file_id}": { + "get": { + "tags": [ + "audio" + ], + "summary": "Get Waveform Rms", + "description": "API endpoint vẽ RMS Waveform (mượt hơn peak).", + "operationId": "get_waveform_rms_api_v1_audio_waveform_rms__file_id__get", + "parameters": [ + { + "name": "file_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "File Id" + } + }, + { + "name": "num_points", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 4000, + "minimum": 50, + "default": 800, + "title": "Num Points" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/audio/analyze-ai": { + "post": { + "tags": [ + "audio" + ], + "summary": "Analyze Audio With Ai", + "description": "API endpoint phân tích cấu trúc khuôn nhạc bằng AI (Week 4).\nGọi OpenAI Compatible API (DeepSeek/Ollama) để phân đoạn bố cục.", + "operationId": "analyze_audio_with_ai_api_v1_audio_analyze_ai_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AIAnalysisRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/audio/export": { + "post": { + "tags": [ + "audio" + ], + "summary": "Export Audio", + "description": "API endpoint xuất tệp âm thanh sang nhiều định dạng (WAV/MP3/OGG).", + "operationId": "export_audio_api_v1_audio_export_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExportRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/audio/export/midi": { + "post": { + "tags": [ + "audio" + ], + "summary": "Export Midi", + "operationId": "export_midi_api_v1_audio_export_midi_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MidiExportRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/audio/ai-scan": { + "post": { + "tags": [ + "audio" + ], + "summary": "Ai Scan Audio", + "description": "17_AI_SCAN.md Feature 1: AI Loop Scan & Automated Marker Labeling.\nUses AIDSPEngine to find optimal recurring loop region with zero-crossing alignment.", + "operationId": "ai_scan_audio_api_v1_audio_ai_scan_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AIScanRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/audio/ai-cut": { + "post": { + "tags": [ + "audio" + ], + "summary": "Ai Cut Audio", + "description": "17_AI_SCAN.md Feature 2: Fade-Free AI Cut (Zero-Crossing Aligned Slicing).\nExecutes raw binary sample slice at exact zero-crossing coordinates.", + "operationId": "ai_cut_audio_api_v1_audio_ai_cut_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AICutRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/audio/python-tool": { + "post": { + "tags": [ + "audio" + ], + "summary": "Run Python Dsp Tool", + "description": "Non-AI Python DSP Tools endpoint.\nHandles normalize peak, invert phase, swap channels, zero-crossing align, and synth wave generation.", + "operationId": "run_python_dsp_tool_api_v1_audio_python_tool_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PythonToolRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/audio/my-files": { + "post": { + "tags": [ + "audio" + ], + "summary": "List User Files", + "operationId": "list_user_files_api_v1_audio_my_files_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MyFilesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/audio/reference-analyze": { + "post": { + "tags": [ + "audio" + ], + "summary": "Analyze Reference", + "description": "Gap 11: A/B reference track - analyze LUFS/dBTP/centroid of a\nreference file (local path from media/browse, or uploaded file_id),\nreturn compare metrics + playback url for the A/B toggle.", + "operationId": "analyze_reference_api_v1_audio_reference_analyze_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReferenceAnalyzeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/audio/my-files/{file_id}": { + "delete": { + "tags": [ + "audio" + ], + "summary": "Delete User File", + "operationId": "delete_user_file_api_v1_audio_my_files__file_id__delete", + "parameters": [ + { + "name": "file_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "File Id" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/audio/tasks/{task_id}": { + "get": { + "tags": [ + "tasks" + ], + "summary": "Get Task Status", + "operationId": "get_task_status_api_v1_audio_tasks__task_id__get", + "parameters": [ + { + "name": "task_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Task Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/multitrack/mix": { + "post": { + "tags": [ + "multitrack" + ], + "summary": "Mix Multitrack Session", + "description": "API endpoint để xử lý hòa âm đa kênh (Multitrack Mixdown).\nNhận cấu hình JSON từ Client và gửi task xuống Celery Worker.", + "operationId": "mix_multitrack_session_api_v1_multitrack_mix_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MultitrackSessionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/multitrack/process-session": { + "post": { + "tags": [ + "multitrack" + ], + "summary": "Process Session", + "description": "API endpoint để xử lý toàn bộ session với nhiều tracks và clips.\nXử lý từng clip, sau đó hòa âm tất cả tracks lại với nhau.", + "operationId": "process_session_api_v1_multitrack_process_session_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MultitrackSessionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/auth/login": { + "post": { + "tags": [ + "auth" + ], + "summary": "Login", + "operationId": "login_api_v1_auth_login_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/auth/local": { + "post": { + "tags": [ + "auth" + ], + "summary": "Local Login", + "description": "Auto-login standalone: chỉ cho phép từ loopback (máy chạy engine).\nClient LAN nhận 403 → buộc đăng nhập mật khẩu qua /login.", + "operationId": "local_login_api_v1_auth_local_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/api/v1/auth/register": { + "post": { + "tags": [ + "auth" + ], + "summary": "Register", + "operationId": "register_api_v1_auth_register_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/auth/change-password": { + "post": { + "tags": [ + "auth" + ], + "summary": "Change Password", + "operationId": "change_password_api_v1_auth_change_password_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangePasswordRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/auth/profile": { + "get": { + "tags": [ + "auth" + ], + "summary": "Get Profile", + "operationId": "get_profile_api_v1_auth_profile_get", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/auth/first-time": { + "get": { + "tags": [ + "auth" + ], + "summary": "Auth First Time", + "operationId": "auth_first_time_api_v1_auth_first_time_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/api/v1/admin/users": { + "get": { + "tags": [ + "admin" + ], + "summary": "List Users", + "operationId": "list_users_api_v1_admin_users_get", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": [ + "admin" + ], + "summary": "Create User", + "operationId": "create_user_api_v1_admin_users_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/admin/users/{user_id}/role": { + "put": { + "tags": [ + "admin" + ], + "summary": "Update User Role", + "operationId": "update_user_role_api_v1_admin_users__user_id__role_put", + "parameters": [ + { + "name": "user_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "User Id" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserRoleRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/admin/quotas/{user_id}": { + "put": { + "tags": [ + "admin" + ], + "summary": "Update User Quota", + "operationId": "update_user_quota_api_v1_admin_quotas__user_id__put", + "parameters": [ + { + "name": "user_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "User Id" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserQuotaRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/admin/users/{user_id}": { + "delete": { + "tags": [ + "admin" + ], + "summary": "Delete User", + "operationId": "delete_user_api_v1_admin_users__user_id__delete", + "parameters": [ + { + "name": "user_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "User Id" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/projects/temp": { + "post": { + "tags": [ + "projects" + ], + "summary": "Save Temp Project", + "operationId": "save_temp_project_api_v1_projects_temp_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SaveTempProjectRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": [ + "projects" + ], + "summary": "Get Temp Project", + "operationId": "get_temp_project_api_v1_projects_temp_get", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/projects/temp/revision": { + "get": { + "tags": [ + "projects" + ], + "summary": "Temp Project Revision", + "description": "Revision nhẹ của bản temp: updated_at + client_id — frontend poll 2-3s\nđể realtime sync giữa LAN browser và Tauri standalone (không tải full\ndata_json mỗi lần). client_id = client ghi bản cuối → client khác bỏ qua\nbản do CHÍNH NÓ ghi (chống ping-pong).", + "operationId": "temp_project_revision_api_v1_projects_temp_revision_get", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/projects/save-to-disk": { + "post": { + "tags": [ + "projects" + ], + "summary": "Save Project To Disk", + "description": "Ctrl-S trên desktop: lưu project ra THƯ MỤC CỦA HỆ ĐIỀU HÀNH\n(Documents/SonicForgeDAW/Projects — bản desktop). Docker/headless KHÔNG\ndùng endpoint này (frontend lưu Cloud). Auth optional — desktop có thể\nchưa login.", + "operationId": "save_project_to_disk_api_v1_projects_save_to_disk_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SaveProjectRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/projects/disk": { + "get": { + "tags": [ + "projects" + ], + "summary": "List Disk Projects", + "description": "Danh sách file Ctrl-S desktop (Documents/SonicForgeDAW/Projects),\nmới nhất trước. Auth optional.", + "operationId": "list_disk_projects_api_v1_projects_disk_get", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/projects/disk/{filename}": { + "get": { + "tags": [ + "projects" + ], + "summary": "Get Disk Project", + "description": "Đọc nội dung 1 file dự án trên đĩa (mở lại sau khi restart).", + "operationId": "get_disk_project_api_v1_projects_disk__filename__get", + "parameters": [ + { + "name": "filename", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Filename" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/projects/cloud": { + "post": { + "tags": [ + "projects" + ], + "summary": "Save Cloud Project", + "operationId": "save_cloud_project_api_v1_projects_cloud_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SaveProjectRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": [ + "projects" + ], + "summary": "List Cloud Projects", + "operationId": "list_cloud_projects_api_v1_projects_cloud_get", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/projects/cloud/{project_id}": { + "get": { + "tags": [ + "projects" + ], + "summary": "Get Cloud Project", + "operationId": "get_cloud_project_api_v1_projects_cloud__project_id__get", + "parameters": [ + { + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Project Id" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "projects" + ], + "summary": "Delete Cloud Project", + "operationId": "delete_cloud_project_api_v1_projects_cloud__project_id__delete", + "parameters": [ + { + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Project Id" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "put": { + "tags": [ + "projects" + ], + "summary": "Update Cloud Project", + "operationId": "update_cloud_project_api_v1_projects_cloud__project_id__put", + "parameters": [ + { + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Project Id" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SaveProjectRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/projects/cloud/{project_id}/backup": { + "post": { + "tags": [ + "projects" + ], + "summary": "Create Project Backup", + "operationId": "create_project_backup_api_v1_projects_cloud__project_id__backup_post", + "parameters": [ + { + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Project Id" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/projects/cloud/{project_id}/backups": { + "get": { + "tags": [ + "projects" + ], + "summary": "List Project Backups", + "operationId": "list_project_backups_api_v1_projects_cloud__project_id__backups_get", + "parameters": [ + { + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Project Id" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/projects/cloud/backups/{backup_id}": { + "delete": { + "tags": [ + "projects" + ], + "summary": "Delete Project Backup", + "operationId": "delete_project_backup_api_v1_projects_cloud_backups__backup_id__delete", + "parameters": [ + { + "name": "backup_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Backup Id" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/projects/cloud/backups/cleanup": { + "post": { + "tags": [ + "projects" + ], + "summary": "Cleanup All Backups", + "operationId": "cleanup_all_backups_api_v1_projects_cloud_backups_cleanup_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CleanupBackupsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/projects/cloud/{project_id}/render": { + "post": { + "tags": [ + "projects" + ], + "summary": "Render Project Endpoint", + "operationId": "render_project_endpoint_api_v1_projects_cloud__project_id__render_post", + "parameters": [ + { + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Project Id" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RenderProjectRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/user/preferences": { + "get": { + "tags": [ + "user_config" + ], + "summary": "Get User Preferences", + "operationId": "get_user_preferences_api_v1_user_preferences_get", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": [ + "user_config" + ], + "summary": "Save User Preferences", + "operationId": "save_user_preferences_api_v1_user_preferences_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SavePreferencesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/user/config/ai": { + "get": { + "tags": [ + "user_config" + ], + "summary": "Get User Ai Config", + "operationId": "get_user_ai_config_api_v1_user_config_ai_get", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": [ + "user_config" + ], + "summary": "Save User Ai Config", + "operationId": "save_user_ai_config_api_v1_user_config_ai_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SaveAIConfigRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/ai/proxy": { + "post": { + "tags": [ + "ai" + ], + "summary": "Proxy Llm", + "operationId": "proxy_llm_api_v1_ai_proxy_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProxyRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/ai/presets": { + "get": { + "tags": [ + "ai" + ], + "summary": "List Presets", + "operationId": "list_presets_api_v1_ai_presets_get", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": [ + "ai" + ], + "summary": "Save Preset", + "operationId": "save_preset_api_v1_ai_presets_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AIPresetSchema" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/ai/presets/{preset_id}": { + "delete": { + "tags": [ + "ai" + ], + "summary": "Delete Preset", + "operationId": "delete_preset_api_v1_ai_presets__preset_id__delete", + "parameters": [ + { + "name": "preset_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Preset Id" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/dirs": { + "get": { + "tags": [ + "plugins" + ], + "summary": "Get Plugin Dirs", + "operationId": "get_plugin_dirs_api_v1_plugins_dirs_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + }, + "post": { + "tags": [ + "plugins" + ], + "summary": "Save Plugin Dirs", + "operationId": "save_plugin_dirs_api_v1_plugins_dirs_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DirsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/scan": { + "post": { + "tags": [ + "plugins" + ], + "summary": "Scan Plugin Dirs", + "description": "Scan các dir hiệu lực (env + user override): cập nhật catalog\nsoundfont (inspector/scanner) + liệt kê VST. Trả về danh sách riêng rẽ\nVST (vst_found) + SoundFont (soundfonts) theo từng thư mục user khai báo.", + "operationId": "scan_plugin_dirs_api_v1_plugins_scan_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/available": { + "get": { + "tags": [ + "plugins" + ], + "summary": "List Plugins", + "operationId": "list_plugins_api_v1_plugins_available_get", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/pick-dir": { + "post": { + "tags": [ + "plugins" + ], + "summary": "Pick Plugin Directory", + "description": "Mở NATIVE folder picker. Không cần auth (desktop local, chỉ mở dialog).\nTrả {\"path\": \"\"} hoặc {\"path\": None} (hủy/không có dialog).\nĐịnh nghĩa def (sync) → FastAPI chạy trong threadpool — không block loop\ntrong lúc user chọn thư mục (có thể mất phút).", + "operationId": "pick_plugin_directory_api_v1_plugins_pick_dir_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/api/v1/plugins/default-soundfonts": { + "get": { + "tags": [ + "plugins" + ], + "summary": "List Default Soundfonts", + "operationId": "list_default_soundfonts_api_v1_plugins_default_soundfonts_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/api/v1/plugins/soundfonts/catalog": { + "get": { + "tags": [ + "plugins" + ], + "summary": "Soundfont Catalog", + "operationId": "soundfont_catalog_api_v1_plugins_soundfonts_catalog_get", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/soundfont-instruments/{sf_id}": { + "get": { + "tags": [ + "plugins" + ], + "summary": "List Soundfont Instruments", + "operationId": "list_soundfont_instruments_api_v1_plugins_soundfont_instruments__sf_id__get", + "parameters": [ + { + "name": "sf_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Sf Id" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/upload-soundfont": { + "post": { + "tags": [ + "plugins" + ], + "summary": "Upload Soundfont", + "operationId": "upload_soundfont_api_v1_plugins_upload_soundfont_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_upload_soundfont_api_v1_plugins_upload_soundfont_post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/soundfont/{sf_id}": { + "delete": { + "tags": [ + "plugins" + ], + "summary": "Delete Soundfont", + "operationId": "delete_soundfont_api_v1_plugins_soundfont__sf_id__delete", + "parameters": [ + { + "name": "sf_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Sf Id" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/soundfonts/download/{sf_id}": { + "get": { + "tags": [ + "plugins" + ], + "summary": "Download Soundfont Asset", + "operationId": "download_soundfont_asset_api_v1_plugins_soundfonts_download__sf_id__get", + "parameters": [ + { + "name": "sf_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Sf Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/fx-gui": { + "post": { + "tags": [ + "plugins" + ], + "summary": "Open Fx Gui", + "description": "Mở native GUI của VST FX (mastering chain / FX rack) qua bridge.\n\nBridge có mode `--open-fx-gui ` (Windows): spawn tiến trình riêng\ngiữ vòng lặp message pump, tạo window 800x600 + attach IPlugView của plugin.\nĐây là GUI tách rời — user chỉnh thông số, đóng window là bridge thoát\n(exit 0). Không spawn 2 lần cho cùng plugin đang mở (tránh trùng GUI).", + "operationId": "open_fx_gui_api_v1_plugins_fx_gui_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FxGuiRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/fx-gui/close": { + "post": { + "tags": [ + "plugins" + ], + "summary": "Close Fx Gui", + "description": "Đóng bridge fx-gui đang chạy cho plugin (embed mode) — frontend gọi khi\nuser đóng panel embed. Không lỗi nếu plugin chưa mở (closed=False).", + "operationId": "close_fx_gui_api_v1_plugins_fx_gui_close_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FxGuiCloseRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/fx-gui/hide": { + "post": { + "tags": [ + "plugins" + ], + "summary": "Hide Fx Gui", + "description": "Ẩn GUI native (ed.hide — giữ editor + instance, mở lại nhanh). Chỉ\náp dụng editor in-process fxrt; GUI spawn mode (HTTP bridge) tự có /hide\nriêng — trả applied=False không lỗi.", + "operationId": "hide_fx_gui_api_v1_plugins_fx_gui_hide_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FxGuiNativeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/fx-gui/show": { + "post": { + "tags": [ + "plugins" + ], + "summary": "Show Fx Gui", + "description": "Hiện lại GUI native (ed.show).", + "operationId": "show_fx_gui_api_v1_plugins_fx_gui_show_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FxGuiNativeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/fx-gui/rect": { + "post": { + "tags": [ + "plugins" + ], + "summary": "Fx Gui Rect", + "description": "M7: anchor PluginWindow native vào rect panel embed (SetWindowPos phía\nC++ khi rect đổi — mirror FxGuiServer /show của float/embed). Frontend đo\nrect panel mỗi 250ms và gửi; chỉ áp dụng editor in-process fxrt — GUI spawn\nmode (HTTP bridge) tự có /show riêng, trả applied=False không lỗi.", + "operationId": "fx_gui_rect_api_v1_plugins_fx_gui_rect_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FxGuiRectRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/preview": { + "post": { + "tags": [ + "plugins" + ], + "summary": "Preview Instrument", + "description": "Quick-render preview VSTi (âm thật, cùng code path với export).", + "operationId": "preview_instrument_api_v1_plugins_preview_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PreviewRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/midi-render": { + "post": { + "tags": [ + "plugins" + ], + "summary": "Midi Render", + "description": "Export MIDI notes → WAV với âm của VSTi instrument (lưu vào processed).\n\nPreview/export MIDI notes với âm VSTi trước đây KHÔNG thực hiện được:\n- preview realtime chỉ phát qua loa hệ thống (không vào audio graph DAW)\n- clientSideExport chỉ render soundfont (midiCache = FluidSynth WASM)\n- /plugins/preview có sẵn nhưng frontend không gọi\nEndpoint này render offline bằng pedalboard (đúng plugin + preset như\nexport) → trả file_id để UI preview / gán clip vào project / download.", + "operationId": "midi_render_api_v1_plugins_midi_render_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MidiRenderRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/fx": { + "get": { + "tags": [ + "plugins" + ], + "summary": "List Fx Plugins", + "description": "List VST effect plugins (is_fx) — native_bridge --scan trên các dir.\n\nChỉ plugin VST3 effect (Ozone, Scaler Audio, ...); instrument/VST2 bị\nbridge loại. refresh=true → quét lại (Plugins Manager → Scan).", + "operationId": "list_fx_plugins_api_v1_plugins_fx_get", + "parameters": [ + { + "name": "refresh", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Refresh" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/fx-render": { + "post": { + "tags": [ + "plugins" + ], + "summary": "Fx Render", + "description": "Áp fx_chain lên WAV đầu vào → WAV mới trong PROCESSED_DIR.", + "operationId": "fx_render_api_v1_plugins_fx_render_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FxRenderRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/fx-realtime/start": { + "post": { + "tags": [ + "plugins" + ], + "summary": "Fx Realtime Start", + "description": "Start bridge realtime FX loop + SHM. Client mở WS /ws/fx-realtime/{id}\n(token qua query param 'token' hoặc Authorization Bearer) rồi gửi block\nstereo float32 interleaved (block_size*2 float = 2048B), nhận block đã xử lý.", + "operationId": "fx_realtime_start_api_v1_plugins_fx_realtime_start_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FxRealtimeStartRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/fx-realtime/stop": { + "post": { + "tags": [ + "plugins" + ], + "summary": "Fx Realtime Stop", + "description": "Dừng session realtime (running=0 → bridge thoát, SHM unlink).", + "operationId": "fx_realtime_stop_api_v1_plugins_fx_realtime_stop_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FxRealtimeStopRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/fx-realtime/set-chain": { + "post": { + "tags": [ + "plugins" + ], + "summary": "Fx Realtime Set Chain", + "description": "Cập nhật chain trên session realtime ĐANG CHẠY — KHÔNG restart session.\nEngine ghi lại job file (seq++); bridge poll seq → RealtimeFxChain::setChain\n(worker load chain mới, swap atomic dưới mutex — audio thread copy\nshared_ptr, không dừng) → load/đổi VST FX + đổi preset GUI không ngắt âm/\ncrackle. Session/WS/SHM giữ nguyên.", + "operationId": "fx_realtime_set_chain_api_v1_plugins_fx_realtime_set_chain_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FxRealtimeSetChainRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/soundfont-render": { + "post": { + "tags": [ + "plugins" + ], + "summary": "Soundfont Render", + "description": "Preview/play MIDI notes bằng âm soundfont thật — native FluidSynth trên OS.\n\nKhác FluidSynthWASM (docker): âm render server-side bằng pyfluidsynth, trả\nWAV để client play. Dùng cho: preview/keyboard, play MIDI item khi chạy\ntransport ở môi trường standalone.", + "operationId": "soundfont_render_api_v1_plugins_soundfont_render_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SoundfontRenderRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/render": { + "post": { + "tags": [ + "plugins" + ], + "summary": "Render Project", + "operationId": "render_project_api_v1_plugins_render_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RenderRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/bridge/status": { + "get": { + "tags": [ + "plugins" + ], + "summary": "Bridge Status", + "description": "E1: trạng thái native bridge. Rust ghi bridge_status (JSON) khi spawn/\nhealth-check; nếu chưa có → probe tail bridge.log (dòng started/exists).", + "operationId": "bridge_status_api_v1_plugins_bridge_status_get", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/bridge/load": { + "post": { + "tags": [ + "plugins" + ], + "summary": "Bridge Load", + "description": "E2: resolve path thật của asset rồi (a) ghi bridge_load.request cho Rust\nwatcher (nếu có) và (b) trả path để JS gọi invoke load_native_instrument\ntrực tiếp — pipeline hoạt động ngay, không chờ watcher.", + "operationId": "bridge_load_api_v1_plugins_bridge_load_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BridgeLoadRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/bridge/log": { + "get": { + "tags": [ + "plugins" + ], + "summary": "Bridge Log", + "description": "E5: tail bridge.log cho UI debug. Log Rust ghi tại %APPDATA%/SonicForgeDAW/logs/bridge.log.", + "operationId": "bridge_log_api_v1_plugins_bridge_log_get", + "parameters": [ + { + "name": "lines", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 2000, + "minimum": 1, + "default": 100, + "title": "Lines" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/plugins/bridge/gui": { + "post": { + "tags": [ + "plugins" + ], + "summary": "Bridge Gui", + "description": "E6: mở native GUI VSTi trên máy chạy server — fallback cho browser LAN\nkhông có Tauri. Pipeline giống invoke open_vst_gui: LOAD_INSTRUMENT rồi\nOPEN_GUI qua SHM daw_vst_bridge.", + "operationId": "bridge_gui_api_v1_plugins_bridge_gui_post", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BridgeGuiRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/files/save-dialog": { + "post": { + "tags": [ + "files" + ], + "summary": "Save File Dialog", + "description": "Mở NATIVE Save As dialog. Không cần auth (desktop local, chỉ mở dialog).\nTrả {\"path\": \"<đường dẫn user chọn>\"} hoặc {\"path\": None} (hủy/không có\ndialog). def (sync) → FastAPI chạy threadpool — không block loop.", + "operationId": "save_file_dialog_api_v1_files_save_dialog_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SaveDialogRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/files/write": { + "post": { + "tags": [ + "files" + ], + "summary": "Write Export File", + "description": "Ghi bytes đã export vào đường dẫn tuyệt đối user chọn qua save dialog.\nTrả {\"path\": ..., \"bytes\": N} — frontend toast kèm đường dẫn thật.", + "operationId": "write_export_file_api_v1_files_write_post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_write_export_file_api_v1_files_write_post" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/media/computer": { + "get": { + "tags": [ + "media" + ], + "summary": "List Computer Roots", + "description": "Liệt kê các ổ đĩa / mount point thật của máy (My Computer).", + "operationId": "list_computer_roots_api_v1_media_computer_get", + "parameters": [ + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/media/browse": { + "get": { + "tags": [ + "media" + ], + "summary": "Browse Directory", + "description": "Liệt kê nội dung một thư mục trên máy: thư mục con + file audio/MIDI.", + "operationId": "browse_directory_api_v1_media_browse_get", + "parameters": [ + { + "name": "path", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Path" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/media/file": { + "get": { + "tags": [ + "media" + ], + "summary": "Serve Local File", + "description": "Phục vụ file audio/MIDI cục bộ để preview.", + "operationId": "serve_local_file_api_v1_media_file_get", + "parameters": [ + { + "name": "path", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Path" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/system/capabilities": { + "get": { + "tags": [ + "system" + ], + "summary": "Get Capabilities", + "description": "Khả năng của môi trường hiện tại (public — cần trước khi đăng nhập).\n\n- runtime: \"desktop\" (server + client cùng 1 máy Windows/macOS) |\n \"headless\" (docker server + browser UI)\n- features.preset_upload: luôn True (upload .vstpreset qua web UI)\n- features.preview_mode: \"quick_render\" (pedalboard render clip ngắn —\n âm thật giống export) | \"wasm\" (Preview Synth trong browser)", + "operationId": "get_capabilities_api_v1_system_capabilities_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/api/v1/presets": { + "get": { + "tags": [ + "presets" + ], + "summary": "List Presets", + "description": "Danh sách preset trong thư viện (public — frontend cần trước login).", + "operationId": "list_presets_api_v1_presets_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/api/v1/presets/upload": { + "post": { + "tags": [ + "presets" + ], + "summary": "Upload Preset", + "description": "Upload preset (.vstpreset / .fxp / .fxb / .dspreset) vào thư viện.", + "operationId": "upload_preset_api_v1_presets_upload_post", + "parameters": [ + { + "name": "plugin_hint", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Plugin Hint" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_upload_preset_api_v1_presets_upload_post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/presets/{preset_id}/download": { + "get": { + "tags": [ + "presets" + ], + "summary": "Download Preset", + "operationId": "download_preset_api_v1_presets__preset_id__download_get", + "parameters": [ + { + "name": "preset_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Preset Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/presets/{preset_id}": { + "delete": { + "tags": [ + "presets" + ], + "summary": "Delete Preset", + "operationId": "delete_preset_api_v1_presets__preset_id__delete", + "parameters": [ + { + "name": "preset_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Preset Id" + } + }, + { + "name": "authorization", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "name": "x-auth-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Auth-Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/health": { + "get": { + "summary": "Health", + "operationId": "health_health_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/": { + "get": { + "summary": "Get Index", + "operationId": "get_index__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/favicon.svg": { + "get": { + "summary": "Get Favicon", + "operationId": "get_favicon_favicon_svg_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/ai-prompt-generator": { + "get": { + "summary": "Get Ai Prompt Generator", + "operationId": "get_ai_prompt_generator_ai_prompt_generator_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "AIAnalysisRequest": { + "properties": { + "file_id": { + "type": "string", + "title": "File Id" + }, + "api_base_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Api Base Url" + }, + "model": { + "type": "string", + "title": "Model", + "default": "deepseek-chat" + } + }, + "type": "object", + "required": [ + "file_id" + ], + "title": "AIAnalysisRequest" + }, + "AICutRequest": { + "properties": { + "source_track_id": { + "type": "string", + "title": "Source Track Id" + }, + "file_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File Id" + }, + "selection_start": { + "type": "number", + "title": "Selection Start" + }, + "selection_end": { + "type": "number", + "title": "Selection End" + } + }, + "type": "object", + "required": [ + "source_track_id", + "selection_start", + "selection_end" + ], + "title": "AICutRequest" + }, + "AIPresetSchema": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Keywords" + }, + "category": { + "type": "string", + "title": "Category", + "default": "General" + }, + "default_bars": { + "type": "integer", + "title": "Default Bars", + "default": 8 + }, + "default_bpm": { + "type": "integer", + "title": "Default Bpm", + "default": 120 + }, + "default_scale": { + "type": "string", + "title": "Default Scale", + "default": "C Major" + }, + "system_instruction_template": { + "type": "string", + "title": "System Instruction Template" + }, + "is_user_defined": { + "type": "boolean", + "title": "Is User Defined", + "default": true + }, + "is_favorite": { + "type": "boolean", + "title": "Is Favorite", + "default": false + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created At" + } + }, + "type": "object", + "required": [ + "id", + "name", + "keywords", + "system_instruction_template" + ], + "title": "AIPresetSchema" + }, + "AIProviderSetting": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "provider_type": { + "type": "string", + "title": "Provider Type" + }, + "api_base_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Api Base Url", + "default": "https://api.openai.com/v1" + }, + "api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Api Key", + "default": "" + }, + "model_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model Name", + "default": "gpt-4o" + }, + "temperature": { + "type": "number", + "title": "Temperature", + "default": 0.7 + }, + "is_active": { + "type": "boolean", + "title": "Is Active", + "default": true + } + }, + "type": "object", + "required": [ + "id", + "name", + "provider_type" + ], + "title": "AIProviderSetting" + }, + "AIScanRequest": { + "properties": { + "track_id": { + "type": "string", + "title": "Track Id" + }, + "file_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File Id" + }, + "min_loop_duration": { + "type": "number", + "title": "Min Loop Duration", + "default": 2.0 + }, + "max_loop_duration": { + "type": "number", + "title": "Max Loop Duration", + "default": 6.0 + } + }, + "type": "object", + "required": [ + "track_id" + ], + "title": "AIScanRequest" + }, + "Body_upload_audio_api_v1_audio_upload_post": { + "properties": { + "file": { + "type": "string", + "contentMediaType": "application/octet-stream", + "title": "File" + } + }, + "type": "object", + "required": [ + "file" + ], + "title": "Body_upload_audio_api_v1_audio_upload_post" + }, + "Body_upload_preset_api_v1_presets_upload_post": { + "properties": { + "file": { + "type": "string", + "contentMediaType": "application/octet-stream", + "title": "File" + } + }, + "type": "object", + "required": [ + "file" + ], + "title": "Body_upload_preset_api_v1_presets_upload_post" + }, + "Body_upload_soundfont_api_v1_plugins_upload_soundfont_post": { + "properties": { + "file": { + "type": "string", + "contentMediaType": "application/octet-stream", + "title": "File" + } + }, + "type": "object", + "required": [ + "file" + ], + "title": "Body_upload_soundfont_api_v1_plugins_upload_soundfont_post" + }, + "Body_write_export_file_api_v1_files_write_post": { + "properties": { + "path": { + "type": "string", + "title": "Path" + }, + "file": { + "type": "string", + "contentMediaType": "application/octet-stream", + "title": "File" + } + }, + "type": "object", + "required": [ + "path", + "file" + ], + "title": "Body_write_export_file_api_v1_files_write_post" + }, + "BridgeGuiRequest": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Path" + }, + "instrumentType": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Instrumenttype" + }, + "channel": { + "type": "integer", + "title": "Channel", + "default": 0 + } + }, + "type": "object", + "required": [ + "name" + ], + "title": "BridgeGuiRequest" + }, + "BridgeLoadRequest": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Path" + }, + "instrumentType": { + "type": "string", + "title": "Instrumenttype", + "default": "SF2" + }, + "channel": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Channel" + } + }, + "type": "object", + "required": [ + "name" + ], + "title": "BridgeLoadRequest" + }, + "ChangePasswordRequest": { + "properties": { + "old_password": { + "type": "string", + "title": "Old Password" + }, + "new_password": { + "type": "string", + "title": "New Password" + } + }, + "type": "object", + "required": [ + "old_password", + "new_password" + ], + "title": "ChangePasswordRequest" + }, + "CleanupBackupsRequest": { + "properties": { + "keep": { + "type": "integer", + "title": "Keep", + "default": 10 + } + }, + "type": "object", + "title": "CleanupBackupsRequest" + }, + "ClipConfig": { + "properties": { + "clip_id": { + "type": "string", + "title": "Clip Id" + }, + "start_time_seconds": { + "type": "number", + "title": "Start Time Seconds" + }, + "end_time_seconds": { + "type": "number", + "title": "End Time Seconds" + }, + "loop_count": { + "type": "integer", + "title": "Loop Count", + "default": 1 + }, + "apply_zero_crossing": { + "type": "boolean", + "title": "Apply Zero Crossing", + "default": true + }, + "fade_in_ms": { + "type": "integer", + "title": "Fade In Ms", + "default": 150 + }, + "fade_out_ms": { + "type": "integer", + "title": "Fade Out Ms", + "default": 150 + } + }, + "type": "object", + "required": [ + "clip_id", + "start_time_seconds", + "end_time_seconds" + ], + "title": "ClipConfig" + }, + "CreateUserRequest": { + "properties": { + "username": { + "type": "string", + "title": "Username" + }, + "email": { + "type": "string", + "title": "Email" + }, + "password": { + "type": "string", + "title": "Password" + }, + "role": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Role", + "default": "standard" + } + }, + "type": "object", + "required": [ + "username", + "email", + "password" + ], + "title": "CreateUserRequest" + }, + "DirsRequest": { + "properties": { + "vst_dir": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Vst Dir" + }, + "soundfont_dir": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Soundfont Dir" + }, + "plugin_dirs": { + "anyOf": [ + { + "items": {}, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Plugin Dirs" + } + }, + "type": "object", + "title": "DirsRequest" + }, + "EditRequest": { + "properties": { + "file_id": { + "type": "string", + "title": "File Id" + }, + "cut_start_ms": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Cut Start Ms" + }, + "cut_end_ms": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Cut End Ms" + }, + "zero_crossing_align": { + "type": "boolean", + "title": "Zero Crossing Align", + "default": true + }, + "loop_count": { + "type": "integer", + "title": "Loop Count", + "default": 1 + }, + "fade_in_ms": { + "type": "number", + "title": "Fade In Ms", + "default": 0.0 + }, + "fade_out_ms": { + "type": "number", + "title": "Fade Out Ms", + "default": 0.0 + }, + "volume_change_db": { + "type": "number", + "title": "Volume Change Db", + "default": 0.0 + } + }, + "type": "object", + "required": [ + "file_id" + ], + "title": "EditRequest" + }, + "ExportRequest": { + "properties": { + "file_id": { + "type": "string", + "title": "File Id" + }, + "format": { + "type": "string", + "title": "Format", + "default": "wav" + }, + "sample_rate": { + "type": "integer", + "title": "Sample Rate", + "default": 44100 + }, + "bit_depth": { + "type": "integer", + "title": "Bit Depth", + "default": 16 + } + }, + "type": "object", + "required": [ + "file_id" + ], + "title": "ExportRequest" + }, + "ExportSettings": { + "properties": { + "sample_rate": { + "type": "integer", + "title": "Sample Rate", + "default": 44100 + }, + "bit_depth": { + "type": "integer", + "title": "Bit Depth", + "default": 16 + }, + "format": { + "type": "string", + "title": "Format", + "default": "wav" + } + }, + "type": "object", + "title": "ExportSettings" + }, + "FxGuiCloseRequest": { + "properties": { + "path": { + "type": "string", + "title": "Path" + } + }, + "type": "object", + "required": [ + "path" + ], + "title": "FxGuiCloseRequest", + "description": "Đóng bridge fx-gui đã mở cho plugin (embed mode)." + }, + "FxGuiNativeRequest": { + "properties": { + "path": { + "type": "string", + "title": "Path" + } + }, + "type": "object", + "required": [ + "path" + ], + "title": "FxGuiNativeRequest", + "description": "Hide/Show GUI native (editor instance DSP trong session fxrt)." + }, + "FxGuiRectBody": { + "properties": { + "x": { + "type": "integer", + "title": "X", + "default": 0 + }, + "y": { + "type": "integer", + "title": "Y", + "default": 0 + }, + "w": { + "type": "integer", + "title": "W", + "default": 0 + }, + "h": { + "type": "integer", + "title": "H", + "default": 0 + } + }, + "type": "object", + "title": "FxGuiRectBody", + "description": "Rect màn hình (px vật lý, gốc trên-trái; x/y âm = màn hình trái)." + }, + "FxGuiRectRequest": { + "properties": { + "path": { + "type": "string", + "title": "Path" + }, + "rect": { + "$ref": "#/components/schemas/FxGuiRectBody", + "default": { + "x": 0, + "y": 0, + "w": 0, + "h": 0 + } + } + }, + "type": "object", + "required": [ + "path" + ], + "title": "FxGuiRectRequest", + "description": "M7: anchor rect cho GUI native — rect panel nơi GUI embed xuất hiện." + }, + "FxGuiRequest": { + "properties": { + "path": { + "type": "string", + "title": "Path" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "embed": { + "type": "boolean", + "title": "Embed", + "default": false + }, + "shm": { + "type": "string", + "title": "Shm", + "default": "" + }, + "preset_b64": { + "type": "string", + "title": "Preset B64", + "default": "" + }, + "master": { + "type": "boolean", + "title": "Master", + "default": false + } + }, + "type": "object", + "required": [ + "path" + ], + "title": "FxGuiRequest", + "description": "Mở native GUI của VST FX qua bridge (Windows desktop)." + }, + "FxRealtimeSetChainRequest": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id", + "default": "" + }, + "fx_chain": { + "items": {}, + "type": "array", + "title": "Fx Chain", + "default": [] + } + }, + "type": "object", + "title": "FxRealtimeSetChainRequest" + }, + "FxRealtimeStartRequest": { + "properties": { + "fx_chain": { + "items": {}, + "type": "array", + "title": "Fx Chain", + "default": [] + }, + "sample_rate": { + "type": "integer", + "title": "Sample Rate", + "default": 44100 + } + }, + "type": "object", + "title": "FxRealtimeStartRequest", + "description": "Start một session FX realtime (master chain qua fx_vst_bridge\n--realtime-fx + SHM ring). Trả session_id để client mở WebSocket\n/ws/fx-realtime/{session_id} đẩy/kéo block audio." + }, + "FxRealtimeStopRequest": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id", + "default": "" + } + }, + "type": "object", + "title": "FxRealtimeStopRequest" + }, + "FxRenderRequest": { + "properties": { + "input_file_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Input File Id" + }, + "input_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Input Path" + }, + "fx_chain": { + "items": {}, + "type": "array", + "title": "Fx Chain", + "default": [] + }, + "sample_rate": { + "type": "integer", + "title": "Sample Rate", + "default": 44100 + } + }, + "type": "object", + "title": "FxRenderRequest", + "description": "Render WAV qua chuỗi FX (VST3 effect + builtin gain/normalize).\n\nDùng cho masterbus (render xong project → Ozone trên bus) và FX Rack\nper-track (preview offline qua bridge; realtime vẫn WebAudio)." + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "LoginRequest": { + "properties": { + "username": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Username", + "default": "admin" + }, + "password": { + "type": "string", + "title": "Password" + } + }, + "type": "object", + "required": [ + "password" + ], + "title": "LoginRequest" + }, + "MidiExportRequest": { + "properties": { + "bpm": { + "type": "number", + "title": "Bpm", + "default": 120.0 + }, + "ppq": { + "type": "integer", + "title": "Ppq", + "default": 480 + }, + "tracks": { + "items": { + "$ref": "#/components/schemas/MidiExportTrackModel" + }, + "type": "array", + "title": "Tracks", + "default": [] + } + }, + "type": "object", + "title": "MidiExportRequest" + }, + "MidiExportTrackModel": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "default": "Track" + }, + "notes": { + "items": { + "$ref": "#/components/schemas/MidiNoteModel" + }, + "type": "array", + "title": "Notes", + "default": [] + } + }, + "type": "object", + "title": "MidiExportTrackModel" + }, + "MidiNoteModel": { + "properties": { + "pitch": { + "type": "integer", + "title": "Pitch", + "default": 60 + }, + "start_beat": { + "type": "number", + "title": "Start Beat", + "default": 0.0 + }, + "duration_beats": { + "type": "number", + "title": "Duration Beats", + "default": 1.0 + }, + "velocity": { + "type": "number", + "title": "Velocity", + "default": 0.8 + } + }, + "type": "object", + "title": "MidiNoteModel" + }, + "MidiRenderRequest": { + "properties": { + "instrument_id": { + "type": "string", + "title": "Instrument Id" + }, + "notes": { + "items": {}, + "type": "array", + "title": "Notes", + "default": [] + }, + "bpm": { + "type": "number", + "title": "Bpm", + "default": 120.0 + }, + "sample_rate": { + "type": "integer", + "title": "Sample Rate", + "default": 44100 + }, + "preset_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Preset Id" + }, + "preset_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Preset Path" + }, + "preset_data": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Preset Data" + } + }, + "type": "object", + "required": [ + "instrument_id" + ], + "title": "MidiRenderRequest", + "description": "Render MIDI notes → audio qua VSTi (âm thật, cùng code path với export).\n\ninstrument_id = plugin_id của track synth_engine (khớp key scan của\nPluginManager). Đây là cầu nối preset → pedalboard: preset chỉnh trong\nnative GUI (.vstpreset) được áp vào pedalboard trước khi render." + }, + "MultitrackSessionRequest": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id" + }, + "export_settings": { + "$ref": "#/components/schemas/ExportSettings" + }, + "tracks": { + "items": { + "$ref": "#/components/schemas/TrackConfig" + }, + "type": "array", + "title": "Tracks" + } + }, + "type": "object", + "required": [ + "session_id", + "export_settings", + "tracks" + ], + "title": "MultitrackSessionRequest" + }, + "MyFilesRequest": { + "properties": { + "active_file_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Active File Ids", + "default": [] + } + }, + "type": "object", + "title": "MyFilesRequest" + }, + "PreviewRequest": { + "properties": { + "instrument_id": { + "type": "string", + "title": "Instrument Id" + }, + "notes": { + "items": {}, + "type": "array", + "title": "Notes", + "default": [] + }, + "bpm": { + "type": "number", + "title": "Bpm", + "default": 120.0 + }, + "sample_rate": { + "type": "integer", + "title": "Sample Rate", + "default": 44100 + }, + "soundfont_bank": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Soundfont Bank", + "default": 0 + }, + "soundfont_program": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Soundfont Program", + "default": 0 + }, + "preset_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Preset Id" + }, + "preset_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Preset Path" + }, + "preset_data": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Preset Data" + } + }, + "type": "object", + "required": [ + "instrument_id" + ], + "title": "PreviewRequest", + "description": "Quick-render preview: pedalboard render clip ngắn bằng ĐÚNG plugin +\npreset (cùng code path với export) → trả wav để browser phát.\nÂm preview = âm export (khác Preview Synth WASM hiện tại)." + }, + "ProxyRequest": { + "properties": { + "url": { + "type": "string", + "title": "Url" + }, + "headers": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Headers", + "default": {} + }, + "body": { + "additionalProperties": true, + "type": "object", + "title": "Body", + "default": {} + } + }, + "type": "object", + "required": [ + "url" + ], + "title": "ProxyRequest" + }, + "PythonToolRequest": { + "properties": { + "tool_type": { + "type": "string", + "title": "Tool Type" + }, + "track_id": { + "type": "string", + "title": "Track Id" + }, + "file_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File Id" + }, + "time_pos": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Time Pos", + "default": 0.0 + }, + "freq": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Freq", + "default": 440.0 + }, + "duration": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Duration", + "default": 2.0 + }, + "wave_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Wave Type", + "default": "sine" + } + }, + "type": "object", + "required": [ + "tool_type", + "track_id" + ], + "title": "PythonToolRequest" + }, + "ReferenceAnalyzeRequest": { + "properties": { + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Path" + }, + "file_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File Id" + } + }, + "type": "object", + "title": "ReferenceAnalyzeRequest" + }, + "RegisterRequest": { + "properties": { + "username": { + "type": "string", + "title": "Username" + }, + "email": { + "type": "string", + "title": "Email" + }, + "password": { + "type": "string", + "title": "Password" + } + }, + "type": "object", + "required": [ + "username", + "email", + "password" + ], + "title": "RegisterRequest" + }, + "RenderProjectRequest": { + "properties": { + "sample_rate": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Sample Rate", + "default": 44100 + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata" + } + }, + "type": "object", + "title": "RenderProjectRequest" + }, + "RenderRequest": { + "properties": { + "project_json": { + "additionalProperties": true, + "type": "object", + "title": "Project Json" + }, + "output_filename": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Output Filename", + "default": "render_output.wav" + }, + "bit_depth": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Bit Depth", + "default": 32 + }, + "sample_rate": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Sample Rate", + "default": 0 + }, + "dither": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Dither", + "default": false + }, + "normalize": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Normalize", + "default": false + }, + "normalize_target": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Normalize Target", + "default": "-1dbtp" + }, + "mono_check": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Mono Check", + "default": false + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata" + } + }, + "type": "object", + "required": [ + "project_json" + ], + "title": "RenderRequest" + }, + "SaveAIConfigRequest": { + "properties": { + "providers": { + "items": { + "$ref": "#/components/schemas/AIProviderSetting" + }, + "type": "array", + "title": "Providers" + } + }, + "type": "object", + "required": [ + "providers" + ], + "title": "SaveAIConfigRequest" + }, + "SaveDialogRequest": { + "properties": { + "default_name": { + "type": "string", + "title": "Default Name", + "default": "untitled" + }, + "filter_name": { + "type": "string", + "title": "Filter Name", + "default": "File" + }, + "filter_ext": { + "type": "string", + "title": "Filter Ext", + "default": "bin" + } + }, + "type": "object", + "title": "SaveDialogRequest" + }, + "SavePreferencesRequest": { + "properties": { + "preferences": { + "additionalProperties": true, + "type": "object", + "title": "Preferences" + } + }, + "type": "object", + "required": [ + "preferences" + ], + "title": "SavePreferencesRequest" + }, + "SaveProjectRequest": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "data_json": { + "type": "string", + "title": "Data Json" + } + }, + "type": "object", + "required": [ + "name", + "data_json" + ], + "title": "SaveProjectRequest" + }, + "SaveTempProjectRequest": { + "properties": { + "data_json": { + "type": "string", + "title": "Data Json" + }, + "client_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Client Id" + } + }, + "type": "object", + "required": [ + "data_json" + ], + "title": "SaveTempProjectRequest" + }, + "SoundfontRenderRequest": { + "properties": { + "soundfont_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Soundfont Id" + }, + "soundfont_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Soundfont Path" + }, + "bank": { + "type": "integer", + "title": "Bank", + "default": 0 + }, + "program": { + "type": "integer", + "title": "Program", + "default": 0 + }, + "notes": { + "items": {}, + "type": "array", + "title": "Notes", + "default": [] + }, + "bpm": { + "type": "number", + "title": "Bpm", + "default": 120.0 + }, + "sample_rate": { + "type": "integer", + "title": "Sample Rate", + "default": 44100 + } + }, + "type": "object", + "title": "SoundfontRenderRequest", + "description": "Render MIDI notes → WAV bằng soundfont THẬT qua native FluidSynth\n(pyfluidsynth) — luồng âm instrument cho môi trường STANDALONE (xử lí\ntrực tiếp trên OS; docker dùng FluidSynthWASM ở client)." + }, + "TrackConfig": { + "properties": { + "track_id": { + "type": "string", + "title": "Track Id" + }, + "file_id": { + "type": "string", + "title": "File Id" + }, + "volume": { + "type": "number", + "title": "Volume", + "default": 1.0 + }, + "muted": { + "type": "boolean", + "title": "Muted", + "default": false + }, + "clips": { + "items": { + "$ref": "#/components/schemas/ClipConfig" + }, + "type": "array", + "title": "Clips", + "default": [] + } + }, + "type": "object", + "required": [ + "track_id", + "file_id" + ], + "title": "TrackConfig" + }, + "UpdateUserQuotaRequest": { + "properties": { + "storage_limit_mb": { + "type": "integer", + "title": "Storage Limit Mb" + }, + "max_tracks": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Tracks", + "default": 16 + } + }, + "type": "object", + "required": [ + "storage_limit_mb" + ], + "title": "UpdateUserQuotaRequest" + }, + "UpdateUserRoleRequest": { + "properties": { + "role": { + "type": "string", + "title": "Role" + }, + "is_active": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Active", + "default": true + } + }, + "type": "object", + "required": [ + "role" + ], + "title": "UpdateUserRoleRequest" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + }, + "input": { + "title": "Input" + }, + "ctx": { + "type": "object", + "title": "Context" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + } + } + } +} \ No newline at end of file diff --git a/md/api_contract/ws_protocol.md b/md/api_contract/ws_protocol.md new file mode 100644 index 0000000..577f516 --- /dev/null +++ b/md/api_contract/ws_protocol.md @@ -0,0 +1,72 @@ +# Hợp đồng realtime FX (WS + worklet) — SonicForgeStudio + +Trích từ `app/api/v1/plugins.py`, `app/core/fx_realtime.py`, +`app/static/js/sf-fx-realtime.js`, `app/static/js/fx-rt-worker.js`, `app/static/js/app.jsx` +tại HEAD `7c236d4`. Đây là đặc tả để port sang Rust (P1/P4). + +## 1. HTTP (REST) + +| Method | Path | Body | Trả về | +|---|---|---|---| +| POST | `/api/v1/plugins/fx-realtime/start` | `{fx_chain: [], sample_rate: 44100}` | `{success, session_id, ws_url, block_size, sample_rate}` | +| POST | `/api/v1/plugins/fx-realtime/stop` | `{session_id}` | `{success, stopped}` | +| POST | `/api/v1/plugins/fx-realtime/set-chain` | `{session_id, fx_chain}` | `{success, updated}` | +| GET | `/api/v1/plugins/bridge/status` | — | trạng thái bridge/SHM | +| POST | `/api/v1/plugins/render` | job render offline (VST) | file kết quả | +| POST | `/api/v1/plugins/soundfont-render` | job render SF2 | WAV | + +- `block_size` = **256 mẫu** (`FXRT_BLOCK` trong `FxRealtimeIPC.h`). +- Auth: Bearer token hoặc `?token=`; desktop loopback không token → auto admin. + +## 2. WebSocket audio + +`GET /api/v1/plugins/ws/fx-realtime/{session_id}?token=...` + +| Hướng | Kiểu frame | Nội dung | +|---|---|---| +| client → server | **binary** | 2048 B = 256 frame × 2 kênh `float32` interleaved | +| server → client | **binary** | như trên, đã qua chain FX | +| client → server | text JSON | `{"cmd":"set_param","slot":n,"key":"...","value":f}` · `{"cmd":"latency","slot":n,"samples":n}` · `{"cmd":"flush"}` | +| server → client | text JSON | `{"cmd":"latency","slot":n,"samples":n}` · `{"cmd":"flush_done"}` | + +Server: `ws_fx_realtime()` (plugins.py ~1344) — vòng lặp receive block → SHM ring → +`fx_vst_bridge --realtime-fx` (hoặc `--juce-fx`) → SHM → send. + +## 3. Worklet ↔ main thread (MessagePort) + +`sf-fx-realtime.js` (AudioWorkletProcessor `sf-fx-realtime`). + +Hằng số: `BLOCK=256`, `BURST=4` (gom 4 block = 1024 mẫu/frame), `FILL=40` (≈213 ms @48k), +`RESUME_FILL=24`, `CAP=64` (≈341 ms), `FADE=512`, `HOLD_FADE=4`, `WRAP_FADE=256`. + +| type | Hướng | Payload | +|---|---|---| +| `__direct` | main → worklet | chuyển `MessagePort` (worker↔worklet trực tiếp, bỏ main thread) | +| `in` | worklet → main/worker | `{data: ArrayBuffer}` 1024 mẫu stereo interleaved | +| `out` | worker → worklet | `{l, r: Float32Array}` block đã xử lý | +| `started` | worklet → main | phát block đầu tiên (đã đủ FILL) | +| `clear_queue` | main → worklet | `{mode: 'play'\|'seek'\|'loop'}` — wipe queue, `hasStarted=false` (mode `play`) | +| `flush` | worklet → main | yêu cầu engine xả ring stale | +| `flush_done` | worker → worklet | engine đã flush | +| `flush_for_stop` | main → worklet | dừng phát (giữ queue) | +| `set_param` | worklet → main | forward nguyên văn tới WS | +| `latency` | main → worklet | `{samples}` — latency chain thật | +| `stats` | worklet → main | `{underruns, holds, fades, silences, capDrops, fadeIns, queueLen}` (≈2 s/lần) | +| `dbg` | worklet → main | `{tag, t, ...}`: `fx_in_first`, `fx_out_first`, `fx_play_first`, `fx_seek_first`, `fx_loop_clear`, `fx_seek_clear`, `fx_wrap_xfade` | +| `dbg_state` | main → worklet | yêu cầu snapshot trạng thái nội bộ | +| `dbg_state_r` | worklet → main | `{k, t, q, playing, hasStarted, silenced, flushPending, stopped, wrapMode, wrapFadeBlocks, latencySamples, resumeFill, inCount, outPos, fadeLeft, holdFadeLeft}` | + +## 4. Đồng bộ playhead / PDC (phía UI, app.jsx) + +- `FX_RT_WET_LAG_SAMPLES = 10240` (= FILL 40 × 256) — độ trễ nội dung khi vào FX path. +- `pdcDelay.delayTime` = 10240/sr khi FX active, 0 khi dry (`fxRtApplyPdc`). +- Crossfade dry↔wet: `masterFxDryGain` / `masterFxGain` rampe `setTargetAtTime(tc=0.025)`. +- `fxRt.crossfade(token)` bất biến: `dryGain + fxGain = 1`. +- Warm bypass: giữ session/node/ws khi PWR OFF giữa lúc play (`fxRtWarmPlayHold`). + +## 5. Ghi chú cho bản Rust (P4) + +Hợp đồng tối thiểu cần giữ để frontend không phải sửa: +1. `POST /fx-realtime/start` → `session_id` + `ws_url`, `block_size=256`. +2. WS binary 2048 B/bản tin, text JSON 3 lệnh (`set_param`, `latency`, `flush`) + 2 phản hồi (`latency`, `flush_done`). +3. Thay hop Python bằng Rust: `worklet → Tauri IPC/Rust → SHM`, giữ nguyên SHM layout với `fx_vst_bridge`. diff --git a/tools/golden_conformance.py b/tools/golden_conformance.py new file mode 100644 index 0000000..c34c3cf --- /dev/null +++ b/tools/golden_conformance.py @@ -0,0 +1,193 @@ +"""P0 golden conformance harness (Python oracle) - SonicForgeStudio. + +Muc dich: sinh manifest hash cho cac ham DSP thuan (deterministic) lam CHUAN +DOI CHIEU khi port sang Rust (P1-P3). Chay: + + python tools/golden_conformance.py --write # ghi manifest + python tools/golden_conformance.py --check # so voi manifest da ghi + +Moi case tra ve bytes canonical (so thuc lam tron 6 chu so thap phan) -> sha256. +Cung input (fixture sinh bang seed co dinh trong code) + cung thuat toan => +cung hash; lech => in ro case nao. +""" +import argparse +import hashlib +import io +import json +import os +import sys +import tempfile + +import numpy as np + +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +sys.path.insert(0, ROOT) + +MANIFEST = os.path.join(ROOT, "md", "api_contract", "golden", "manifest.json") +SR = 48000 + + +# ── fixtures (seed co dinh, khong dung file ngoai) ─────────────────────────── + +def _tone(freq, dur, sr=SR, amp=0.5): + t = np.linspace(0, dur, int(sr * dur), endpoint=False) + return (amp * np.sin(2 * np.pi * freq * t)).astype(np.float32) + + +def _fixture_stereo(): + rng = np.random.default_rng(1234) + n = SR * 2 + t = np.arange(n) / SR + left = (0.4 * np.sin(2 * np.pi * 220 * t) + 0.05 * rng.standard_normal(n)).astype(np.float32) + right = (0.4 * np.sin(2 * np.pi * 330 * t + 0.7) + 0.05 * rng.standard_normal(n)).astype(np.float32) + return np.column_stack([left, right]).astype(np.float32) + + +def _fixture_click_track(): + """Click track 120 BPM de analyzer/beat khong bi suy bien.""" + n = SR * 4 + y = np.zeros(n, dtype=np.float32) + step = SR // 2 # 120 BPM + for i in range(0, n, step): + y[i:i + 64] = 0.8 + return y + + +def _write_wav(dirpath, name, data, sr=SR): + import soundfile as sf + p = os.path.join(dirpath, name) + sf.write(p, data, sr, subtype="FLOAT") + return p + + +# ── canonical hoa + hash ───────────────────────────────────────────────────── + +def _canon(obj): + if isinstance(obj, np.ndarray): + a = np.asarray(obj, dtype=np.float64) + return json.dumps({"dtype": str(obj.dtype), "shape": list(a.shape), + "data": np.round(a.ravel(), 6).tolist()}, + sort_keys=True) + if isinstance(obj, dict): + return json.dumps({k: _canon(v) for k, v in sorted(obj.items())}, sort_keys=True) + if isinstance(obj, (list, tuple)): + return json.dumps([_canon(v) for v in obj]) + if isinstance(obj, (np.floating, float)): + return json.dumps(round(float(obj), 6)) + if isinstance(obj, (np.integer, int)): + return json.dumps(int(obj)) + if isinstance(obj, bytes): + return json.dumps(list(obj)) + return json.dumps(obj, sort_keys=True) + + +def _sha(obj): + return hashlib.sha256(_canon(obj).encode("utf-8")).hexdigest() + + +# ── cac case ───────────────────────────────────────────────────────────────── + +def collect(): + cases = {} + import soundfile as sf + tmp = tempfile.mkdtemp(prefix="sf_golden_") + stereo = _fixture_stereo() + mono = _tone(440.0, 1.0) + p_stereo = _write_wav(tmp, "stereo.wav", stereo) + p_mono = _write_wav(tmp, "mono.wav", mono) + p_click = _write_wav(tmp, "click.wav", _fixture_click_track()) + + from app.core import dsp_utils, audio_editor, analyzer, loudness + + # dsp_utils + targets = [0.0, 0.123, 0.5, 1.37, 1.999] + cases["dsp_utils.find_zero_crossing"] = [ + dsp_utils.find_zero_crossing(stereo[:, 0], SR, tt) for tt in targets] + + orig = stereo[:, 0].copy() + edited = np.roll(orig, 37) + cases["dsp_utils.apply_micro_crossfade"] = dsp_utils.apply_micro_crossfade( + orig, edited, start_sample=SR // 2, fade_len_ms=10, sr=SR) + + cases["dsp_utils.generate_peak_waveform"] = dsp_utils.generate_peak_waveform(p_stereo, 400) + cases["dsp_utils.generate_rms_waveform"] = dsp_utils.generate_rms_waveform(p_stereo, 400) + + # audio_editor + seg = audio_editor.cut_and_loop_segment( + p_stereo, 0.25, 0.75, loop_count=2, fade_in_ms=20, fade_out_ms=20, + volume_db_change=-3.0) + cases["audio_editor.cut_and_loop_segment"] = { + "channels": seg.channels, "frame_rate": seg.frame_rate, + "sample_width": seg.sample_width, "raw": seg.raw_data} + + # bit_depth=32 (IEEE float) — KHONG dither, bit-exact. 16/24-bit dung TPDF + # dither ngau nhien (np.random.default_rng()) nen output KHONG tai lap duoc. + out_wav = os.path.join(tmp, "mix.wav") + tracks_meta = [ + {"file_path": p_stereo, "volume": 0.8, "muted": False}, + {"file_path": p_mono, "volume": 0.6, "muted": False}, + {"file_path": p_click, "volume": 0.4, "muted": True}, + ] + audio_editor.mix_multitrack_session(tracks_meta, out_wav, sample_rate=SR, bit_depth=32) + # Hash theo MAU audio (khong hash byte file): header WAV do pydub/ffmpeg ghi + # co metadata khong on dinh giua cac lan chay, du samples giong het nhau. + y_mix, sr_mix = sf.read(out_wav, dtype="float64") + cases["audio_editor.mix_multitrack_session"] = {"sample_rate": sr_mix, "audio": y_mix} + + # analyzer + cases["analyzer.analyze_audio"] = analyzer.analyze_audio(p_click) + + # loudness + cases["loudness.integrated_lufs"] = loudness.integrated_lufs(stereo, SR) + cases["loudness.true_peak_db"] = loudness.true_peak_db(stereo, SR) + cases["loudness.mono_side_ratio"] = loudness.mono_side_ratio(stereo) + return cases + + +def build_manifest(): + import numpy as np + import scipy + cases = collect() + return { + "schema": 1, + "generated_by": "tools/golden_conformance.py", + "env": {"python": sys.version.split()[0], "numpy": np.__version__, + "scipy": scipy.__version__, "sample_rate": SR}, + "cases": {k: _sha(v) for k, v in cases.items()}, + } + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--write", action="store_true") + ap.add_argument("--check", action="store_true") + args = ap.parse_args() + + manifest = build_manifest() + if args.check: + with io.open(MANIFEST, "r", encoding="utf-8") as f: + ref = json.load(f) + bad = [] + for k, h in ref["cases"].items(): + cur = manifest["cases"].get(k) + if cur != h: + bad.append(k) + extra = [k for k in manifest["cases"] if k not in ref["cases"]] + for k in bad: + print("MISMATCH %s\n ref=%s\n cur=%s" % (k, ref["cases"][k], manifest["cases"][k])) + for k in extra: + print("NEW-CASE %s" % k) + print("cases=%d mismatch=%d new=%d" % (len(ref["cases"]), len(bad), len(extra))) + return 1 if bad else 0 + + os.makedirs(os.path.dirname(MANIFEST), exist_ok=True) + with io.open(MANIFEST, "w", encoding="utf-8", newline="\n") as f: + json.dump(manifest, f, indent=1, sort_keys=True) + print("WROTE %s (%d cases)" % (MANIFEST, len(manifest["cases"]))) + for k, h in sorted(manifest["cases"].items()): + print(" %-45s %s" % (k, h[:16])) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/p0_baseline.py b/tools/p0_baseline.py new file mode 100644 index 0000000..ab31edd --- /dev/null +++ b/tools/p0_baseline.py @@ -0,0 +1,168 @@ +"""P0 baseline metrics - SonicForgeStudio (do truoc khi port Rust). + +Do: kich thuoc bundle, thoi gian cold start + RAM cua daw_engine.exe, +thoi gian import app.main, thoi gian mixdown 4 track 30s. + +Chay: python tools/p0_baseline.py # in ra stdout + ghi md/BASELINE_P0.md + python tools/p0_baseline.py --print # chi in +""" +import argparse +import io +import json +import os +import shutil +import statistics +import subprocess +import sys +import tempfile +import time +import urllib.request + +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +ENGINE_EXE = os.path.join(ROOT, "dist", "daw_engine", "daw_engine.exe") +ENGINE_DIRS = ["dist/daw_engine", "src-tauri/resources/daw_engine"] +OUT_MD = os.path.join(ROOT, "md", "BASELINE_P0.md") +SR = 48000 + + +def dir_size_mb(rel): + p = os.path.join(ROOT, rel) + if not os.path.exists(p): + return None + total = files = 0 + for r, _d, fs in os.walk(p): + for f in fs: + try: + total += os.path.getsize(os.path.join(r, f)) + files += 1 + except OSError: + pass + return {"mb": round(total / 1048576.0, 1), "files": files} + + +def measure_bundle(): + out = {} + for d in ENGINE_DIRS: + s = dir_size_mb(d) + if s: + out[d] = s + for sub in ("torch", "torchvision", "sympy", "networkx", "tensorflow"): + p = os.path.join(ROOT, "dist", "daw_engine", "_internal", sub) + out["has:" + sub] = os.path.exists(p) + return out + + +def _tasklist_mem_kb(pid): + try: + r = subprocess.run(["tasklist", "/FI", "PID eq %d" % pid, "/FO", "CSV", "/NH"], + capture_output=True, text=True, timeout=15) + row = r.stdout.strip().splitlines()[0] + cells = [c.strip('"') for c in row.split('","')] + return int(cells[-1].replace(",", "").replace(" K", "").strip()) + except Exception: + return None + + +def measure_cold_start(timeout_s=90): + if not os.path.exists(ENGINE_EXE): + return {"error": "engine exe khong ton tai: " + ENGINE_EXE} + env = dict(os.environ) + env["SF_PORT"] = "8000" + t0 = time.perf_counter() + proc = subprocess.Popen([ENGINE_EXE], cwd=os.path.dirname(ENGINE_EXE), env=env, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + port = None + try: + while time.perf_counter() - t0 < timeout_s: + if proc.poll() is not None: + return {"error": "engine thoat som rc=%s" % proc.returncode} + for p in range(8000, 8011): + try: + with urllib.request.urlopen("http://127.0.0.1:%d/health" % p, timeout=0.5) as r: + if r.status == 200: + port = p + break + except Exception: + continue + if port: + break + time.sleep(0.05) + ms = round((time.perf_counter() - t0) * 1000.0, 1) + mem = _tasklist_mem_kb(proc.pid) + result = {"startup_ms": ms, "port": port, "rss_mb": round(mem / 1024.0, 1) if mem else None} + finally: + subprocess.run(["taskkill", "/PID", str(proc.pid), "/T", "/F"], + capture_output=True, text=True) + try: + proc.wait(timeout=10) + except Exception: + pass + return result + + +def measure_import_time(): + code = ("import time,sys;t=time.perf_counter();import app.main;" + "print(round((time.perf_counter()-t)*1000,1))") + t0 = time.perf_counter() + r = subprocess.run([sys.executable, "-c", code], cwd=ROOT, capture_output=True, text=True) + wall = round((time.perf_counter() - t0) * 1000.0, 1) + try: + imp = float(r.stdout.strip().splitlines()[-1]) + except Exception: + imp = None + return {"import_app_main_ms": imp, "subprocess_wall_ms": wall} + + +def measure_render(reps=3): + import numpy as np + sys.path.insert(0, ROOT) + from app.core import audio_editor + tmp = tempfile.mkdtemp(prefix="sf_bench_") + dur = 30.0 + n = int(SR * dur) + t = np.arange(n) / SR + paths = [] + for i, f in enumerate((110.0, 220.0, 330.0, 440.0)): + y = (0.3 * np.sin(2 * np.pi * f * t)).astype(np.float32) + p = os.path.join(tmp, "t%d.wav" % i) + import soundfile as sf + sf.write(p, np.column_stack([y, y]), SR, subtype="FLOAT") + paths.append(p) + meta = [{"file_path": p, "volume": 0.8, "muted": False} for p in paths] + times = [] + for i in range(reps): + out = os.path.join(tmp, "mix%d.wav" % i) + t0 = time.perf_counter() + audio_editor.mix_multitrack_session(meta, out, sample_rate=SR, bit_depth=32) + times.append(round((time.perf_counter() - t0) * 1000.0, 1)) + shutil.rmtree(tmp, ignore_errors=True) + return {"tracks": 4, "seconds_each": dur, "sr": SR, + "runs_ms": times, "min_ms": min(times), "median_ms": round(statistics.median(times), 1), + "realtime_factor": round(min(times) / (dur * 1000.0), 4)} + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--print", action="store_true") + args = ap.parse_args() + metrics = { + "date": time.strftime("%Y-%m-%d %H:%M"), + "bundle": measure_bundle(), + "cold_start": measure_cold_start(), + "import": measure_import_time(), + "render_mixdown": measure_render(), + } + text = "# Baseline P0 - SonicForgeStudio (Python engine)\n\n" \ + "Sinh boi `tools/p0_baseline.py` ngay %s. Dung lam moc so sanh khi port Rust.\n\n```json\n%s\n```\n" % ( + metrics["date"], json.dumps(metrics, indent=1, ensure_ascii=False)) + print(text) + if not args.print: + os.makedirs(os.path.dirname(OUT_MD), exist_ok=True) + with io.open(OUT_MD, "w", encoding="utf-8", newline="\n") as f: + f.write(text) + print("WROTE", OUT_MD) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())