feat: add SoundFont inspection engine + AI instrument schema

- SoundFontInspector (sf2utils) scans .sf2, generates full/condensed catalog
- GET /api/v1/plugins/soundfonts/catalog with lazy init + cache invalidation
- AI tool generate_multitrack_midi now requires soundfont_id/bank/program
- Condensed catalog auto-injected into AI system prompt with bank rules
- Server render: FluidSynth program_select uses bank/program + channel routing (drums→ch9)
- VST3 pedalboard path inserts CC0 bank select + program change before notes
- DecentSamplerManager loads .dspreset with CWD fix for relative sample paths
- Pianobook render branch in render_engine.py
- Client SonicSF: controllerChange, programChange, applyAITrackInstrument
- Post-AI track creation applies instrument via applyAITrackInstrument
- Background cache rescan on .sf2 upload, frontend re-fetches catalog
- libcurl4 + VST3 dirs in Dockerfile
This commit is contained in:
2026-07-26 12:36:48 +07:00
parent f16467eba1
commit 89c7237379
12 changed files with 361 additions and 13 deletions
+28 -1
View File
@@ -1,10 +1,11 @@
import os, uuid, json, tempfile
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks
from pydantic import BaseModel
from typing import Optional, Any
from app.config import settings
from app.core.vst_engine import PluginManager, HAS_PEDALBOARD, HAS_PYFLUIDSYNTH
from app.core.render_engine import PythonRenderEngine
from app.core.soundfont_inspector import SoundFontInspector
from app.api.v1.auth import get_current_user
router = APIRouter()
@@ -12,6 +13,16 @@ router = APIRouter()
UPLOAD_SF_DIR = os.path.join(settings.STORAGE_DIR, "soundfonts")
os.makedirs(UPLOAD_SF_DIR, exist_ok=True)
SYSTEM_SF_DIR = "/opt/daw_engine/soundfonts"
_inspector = None
def get_inspector():
global _inspector
if _inspector is None:
_inspector = SoundFontInspector(system_sf_dir=SYSTEM_SF_DIR, upload_sf_dir=UPLOAD_SF_DIR)
return _inspector
@router.get("/available")
async def list_plugins(current_user: dict = Depends(get_current_user)):
@@ -35,6 +46,14 @@ async def list_default_soundfonts():
return result
@router.get("/soundfonts/catalog")
async def soundfont_catalog(current_user: dict = Depends(get_current_user)):
inspector = get_inspector()
full_catalog = inspector.get_catalog()
condensed_catalog = inspector.get_condensed_catalog_summary()
return {"full_catalog": full_catalog, "condensed_catalog": condensed_catalog}
@router.get("/soundfont-instruments/{sf_id}")
async def list_soundfont_instruments(sf_id: str, current_user: dict = Depends(get_current_user)):
pm = PluginManager(upload_sf_dir=UPLOAD_SF_DIR)
@@ -45,6 +64,7 @@ async def list_soundfont_instruments(sf_id: str, current_user: dict = Depends(ge
@router.post("/upload-soundfont")
async def upload_soundfont(
file: UploadFile = File(...),
background_tasks: BackgroundTasks = None,
current_user: dict = Depends(get_current_user)
):
if not (file.filename and (file.filename.endswith(".sf2") or file.filename.endswith(".sf3"))):
@@ -69,6 +89,11 @@ async def upload_soundfont(
import json
json.dump({"original_name": file.filename, "uuid": file_uuid, "file": file_id}, f)
inspector = get_inspector()
inspector.invalidate_catalog_cache()
if background_tasks:
background_tasks.add_task(inspector.generate_full_catalog)
return {"id": file_id, "name": file.filename, "path": dest_path, "size_bytes": len(contents)}
@@ -93,6 +118,8 @@ async def delete_soundfont(sf_id: str, current_user: dict = Depends(get_current_
break
if not deleted:
raise HTTPException(status_code=404, detail="SoundFont not found")
inspector = get_inspector()
inspector.invalidate_catalog_cache()
return {"deleted": True, "sf_id": sf_id}