FEAT: cài đặt các thư viện để chuẩn bị build exe trên window

This commit is contained in:
2026-08-08 08:46:15 +07:00
parent 9ef622f29a
commit 029ed1c456
24 changed files with 575 additions and 13 deletions
+40
View File
@@ -0,0 +1,40 @@
import os
import platform
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from typing import Optional
from app.core.vst_scanner import NativePluginScanner
from app.api.v1.auth import get_current_user
router = APIRouter()
# Thư mục mặc định theo OS (spec desktop VST 2026-08-08)
DEFAULT_PLUGIN_DIRS = {
"Linux": ["/opt/daw_engine/vst3", os.path.expanduser("~/.vst3")],
"Windows": [os.path.expandvars(r"%ProgramFiles%\Common Files\VST3")],
"Darwin": ["/Library/Audio/Plug-Ins/Components", os.path.expanduser("~/Library/Audio/Plug-Ins/Components")],
}
class PluginScanRequest(BaseModel):
custom_directories: Optional[list] = None
scan_formats: Optional[list] = None
force_rescan: Optional[bool] = False
@router.post("/plugins/scan")
async def scan_desktop_plugins(req: PluginScanRequest, current_user: dict = Depends(get_current_user)):
"""Quét VST3/AU từ thư mục custom — spec: POST /api/v1/desktop/plugins/scan."""
os_name = platform.system()
dirs = req.custom_directories or DEFAULT_PLUGIN_DIRS.get(os_name, ["/opt/daw_engine/vst3"])
if isinstance(dirs, str):
dirs = [dirs]
scanner = NativePluginScanner()
plugins = scanner.scan_directories(dirs)
return {
"status": "success",
"os": os_name,
"scanned_directories": dirs,
"total_found": len(plugins),
"plugins": plugins,
}
+1
View File
@@ -18,6 +18,7 @@ os.makedirs(UPLOAD_SF_DIR, exist_ok=True)
SYSTEM_SF_DIR = "/opt/daw_engine/soundfonts"
_inspector = None
_scanner = None
+3 -1
View File
@@ -105,6 +105,7 @@ class PythonRenderEngine:
# Parse synth_engine struct (Task C) — fall back to flat fields
se = track.get("synth_engine", {}) or {}
instrument_id = se.get("plugin_id") or track.get("instrument_id", "") or track.get("instrument", "")
vst_path = se.get("plugin_path") or "" # spec desktop: load VST3 từ plugin_path
instrument_source = se.get("type") or track.get("instrument_source", "soundfont")
soundfont_bank = se.get("soundfont_bank") if se.get("soundfont_bank") is not None else track.get("soundfont_bank", 0)
soundfont_program = se.get("soundfont_program") if se.get("soundfont_program") is not None else track.get("soundfont_program", 0)
@@ -189,7 +190,8 @@ class PythonRenderEngine:
if midi_events:
try:
plugin_mgr = PluginManager()
vst = plugin_mgr.load_vst(instrument_id) if instrument_id else None
# Ưu tiên plugin_path (spec desktop) — fallback plugin_id theo tên
vst = plugin_mgr.load_vst(vst_path or instrument_id) if (vst_path or instrument_id) else None
if instrument_source == "pianobook":
dspreset_path = track.get("dspreset_path", "")
+16 -2
View File
@@ -216,6 +216,19 @@ class PluginManager:
def load_vst(self, plugin_name: str, preset_data: dict = None):
if not HAS_PEDALBOARD:
return None
# Ưu tiên PATH trực tiếp (spec desktop 2026-08-08: synth_engine.plugin_path)
if plugin_name and (plugin_name.endswith(".vst3") or plugin_name.endswith(".so")
or plugin_name.endswith(".component") or "/" in plugin_name or "\\" in plugin_name):
if os.path.exists(plugin_name):
vst = VST3Plugin(plugin_name)
if preset_data:
for k, v in preset_data.items():
try:
setattr(vst, k, v)
except Exception:
pass
return vst
return None
plugins = self._scan_plugins()
if plugin_name not in plugins:
return None
@@ -305,8 +318,9 @@ class PluginManager:
def list_available(self) -> dict:
return {
"vst_instruments": [
{"id": k, "name": k, "type": "VST3", "has_native_support": HAS_PEDALBOARD}
for k in self._scan_plugins().keys()
{"id": "vst3_" + k.lower().replace(" ", "_"), "name": k, "type": "VST3",
"path": p, "has_native_support": HAS_PEDALBOARD}
for k, p in self._scan_plugins().items()
],
"soundfonts": self._scan_soundfonts()
}
+86
View File
@@ -0,0 +1,86 @@
import os
import platform
import logging
from typing import List, Dict, Any
logger = logging.getLogger(__name__)
class NativePluginScanner:
"""Quét thư mục custom chứa VST3 (.vst3) / Audio Unit (.component trên macOS)
— spec DESKTOP VST (2026-08-08): POST /api/v1/desktop/plugins/scan."""
def __init__(self):
self.os_type = platform.system() # 'Linux', 'Windows', 'Darwin'
def scan_directories(self, target_paths: List[str]) -> List[Dict[str, Any]]:
found_plugins = []
seen = set()
for base_path in target_paths:
if not base_path:
continue
if not os.path.exists(base_path):
logger.warning(f"Scan path does not exist: {base_path}")
continue
for root, dirs, files in os.walk(base_path):
for dir_name in dirs:
full_path = os.path.join(root, dir_name)
# VST3 = bundle directory *.vst3
if dir_name.endswith('.vst3'):
plugin_name = dir_name[:-5]
pid = f"vst3_{plugin_name.lower().replace(' ', '_')}"
if pid in seen:
continue
seen.add(pid)
found_plugins.append({
"id": pid,
"name": plugin_name,
"type": "VST3",
"format": "VST3",
"category": "Instrument",
"vendor": self._read_vendor(full_path) or None,
"path": full_path,
"is_instrument": True,
"has_native_gui": True
})
# Audio Unit = bundle *.component (macOS only)
elif dir_name.endswith('.component') and self.os_type == 'Darwin':
plugin_name = dir_name[:-10]
pid = f"au_{plugin_name.lower().replace(' ', '_')}"
if pid in seen:
continue
seen.add(pid)
found_plugins.append({
"id": pid,
"name": plugin_name,
"type": "AU",
"format": "AudioUnit",
"category": "Instrument",
"vendor": self._read_vendor(full_path) or None,
"path": full_path,
"is_instrument": True,
"has_native_gui": True
})
return found_plugins
@staticmethod
def _read_vendor(bundle_path: str) -> str:
"""Đọc vendor từ Info.plist nếu có (macOS) — best-effort."""
try:
plist = os.path.join(bundle_path, "Contents", "Info.plist")
if os.path.exists(plist):
with open(plist, "rb") as f:
raw = f.read(65536)
import plistlib
try:
data = plistlib.loads(raw)
return data.get("CFBundleManufacturer") or data.get("NSHumanReadableCopyright")
except Exception:
return None
except Exception:
return None
return None
+2
View File
@@ -17,6 +17,7 @@ from app.api.v1.user_config import router as user_config_router
from app.api.v1.ai_proxy import router as ai_proxy_router
from app.api.v1.ai_presets import router as ai_presets_router
from app.api.v1.plugins import router as plugins_router
from app.api.v1.desktop_plugins import router as desktop_plugins_router
from app.api.v1.media import router as media_router
from app.core.auth import seed_admin
from app.core.soundfont_scanner import SoundFontAutoScanner
@@ -72,6 +73,7 @@ app.include_router(user_config_router, prefix="/api/v1/user", tags=["user_config
app.include_router(ai_proxy_router, prefix="/api/v1/ai", tags=["ai"])
app.include_router(ai_presets_router, prefix="/api/v1/ai", tags=["ai"])
app.include_router(plugins_router, prefix="/api/v1/plugins", tags=["plugins"])
app.include_router(desktop_plugins_router, prefix="/api/v1/desktop", tags=["desktop"])
app.include_router(media_router, prefix="/api/v1/media", tags=["media"])
+86 -6
View File
@@ -14193,6 +14193,12 @@ const App = () => {
const [selectedSoundFontId, setSelectedSoundFontId] = useState(null);
const [sfPresets, setSfPresets] = useState(null); // presets from SoundFont
const [instrumentDropdownTrackId, setInstrumentDropdownTrackId] = useState(null);
// Plugin Manager VST scan (spec desktop 2026-08-08)
const [vstScanOpen, setVstScanOpen] = useState(false);
const [vstScanPath, setVstScanPath] = useState('/opt/daw_engine/vst3');
const [vstScanning, setVstScanning] = useState(false);
const [vstPlugins, setVstPlugins] = useState([]);
const [vstScanError, setVstScanError] = useState(null);
const [instrumentDropdownBtnRect, setInstrumentDropdownBtnRect] = useState(null);
const [instrumentSearchQuery, setInstrumentSearchQuery] = useState('');
const [sfPresetSearchQuery, setSfPresetSearchQuery] = useState('');
@@ -14249,8 +14255,9 @@ const App = () => {
"Tinkle Bell","Agogo","Steel Drums","Woodblock","Taiko Drum","Melodic Tom","Synth Drum","Reverse Cymbal",
"Guitar Fret Noise","Breath Noise","Seashore","Bird Tweet","Telephone Ring","Helicopter","Applause","Gunshot"
];
const setTrackInstrumentWithProgram = (trackId, instrumentId, programNumber, displayName, bankNumber) => {
const setTrackInstrumentWithProgram = (trackId, instrumentId, programNumber, displayName, bankNumber, vstMeta) => {
const isSfInstrument = instrumentId && typeof instrumentId === 'string' && instrumentId.startsWith('sf_');
const isVstInstrument = instrumentId && typeof instrumentId === 'string' && (instrumentId.startsWith('vst3_') || instrumentId.startsWith('au_') || (vstMeta && vstMeta.format));
const sfBank = bankNumber !== undefined ? bankNumber : (isSfInstrument ? 0 : undefined);
const sfProg = programNumber !== undefined ? programNumber : undefined;
var mt = activeTracksRef.current || tracks;
@@ -14264,6 +14271,12 @@ const App = () => {
const synthEngine = hasInstrument ? {
type: instrType,
plugin_id: instrumentId,
// spec desktop 2026-08-08: synth_engine đy đ {type, plugin_id, plugin_name, format, plugin_path}
...(isVstInstrument ? {
plugin_name: (vstMeta && vstMeta.plugin_name) || (displayName || instrumentId).replace(/\s*\((VST3|AU|AudioUnit)\)\s*$/, ''),
format: (vstMeta && vstMeta.format) || (instrumentId.startsWith('au_') ? 'AudioUnit' : 'VST3'),
plugin_path: (vstMeta && vstMeta.plugin_path) || ''
} : {}),
soundfont_bank: sfBank !== undefined ? sfBank : 0,
soundfont_program: sfProg !== undefined ? sfProg : 0,
soundfont_id: isSfInstrument ? instrumentId.replace('sf_', '') : ''
@@ -15114,7 +15127,7 @@ const App = () => {
setAiPrompt(newText);
};
const setTrackInstrumentWithUndo = (trackId, instrumentId, displayName, bankNumber, programNumber) => {
const setTrackInstrumentWithUndo = (trackId, instrumentId, displayName, bankNumber, programNumber, vstMeta) => {
const track = activeTracks.find(t => t.id === trackId);
if (!track) return;
const oldInstrumentId = track.instrumentId;
@@ -15136,7 +15149,7 @@ const App = () => {
}
};
if (window.UndoRedoEngine) window.UndoRedoEngine.execute(entry);
setTrackInstrumentWithProgram(trackId, instrumentId, programNumber, displayName, bankNumber);
setTrackInstrumentWithProgram(trackId, instrumentId, programNumber, displayName, bankNumber, vstMeta);
};
const createSectionWithUndo = (trackId, section) => {
@@ -29370,11 +29383,78 @@ STRICT CONSTRAINTS:
filteredInstruments.vst.length > 0 && /*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold" }, "VST Instruments"),
filteredInstruments.vst.map((v, i) => /*#__PURE__*/React.createElement("button", {
key: "vstd_" + i,
onClick: () => { setInstrumentDropdownTrackId(null); setInstrumentDropdownBtnRect(null); setTrackInstrumentWithUndo(instrumentDropdownTrackId, v.id, v.name || v.id); },
onClick: () => { setInstrumentDropdownTrackId(null); setInstrumentDropdownBtnRect(null); setTrackInstrumentWithUndo(instrumentDropdownTrackId, v.id, v.name || v.id, undefined, undefined, { plugin_name: v.name || v.id, format: v.type || 'VST3', plugin_path: v.path || '' }); },
className: "w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-violet-800 text-zinc-200 flex items-center justify-between"
}, /*#__PURE__*/React.createElement("span", { className: "truncate" }, v.name || v.id), /*#__PURE__*/React.createElement("span", { className: "text-[10px] text-cyan-400 shrink-0 ml-1" }, v.type || "VST"))),
(!filteredInstruments.soundfonts.length && !filteredInstruments.vst.length) && /*#__PURE__*/React.createElement("p", { className: "text-xs text-zinc-500 py-4 text-center" }, "Kh\u00f4ng t\u00ecm th\u1ea5y nh\u1ea1c c\u1ee5 n\u00e0o")
)));
/*#__PURE__*/React.createElement("button", {
onClick: () => { setInstrumentDropdownBtnRect(null); setVstScanOpen(true); },
className: "w-full text-left px-3 py-1.5 text-xs bg-purple-900/40 hover:bg-purple-800/60 text-purple-300 border-t border-zinc-700"
}, "\uD83D\uDD0C Quét VST Directory\u2026"),
(!filteredInstruments.soundfonts.length && !filteredInstruments.vst.length) && /*#__PURE__*/React.createElement("p", { className: "text-xs text-zinc-500 py-4 text-center" }, "Kh\u00f4ng t\u00ecm th\u1EA5y nh\u1EA1c c\u1EE5 n\u00E0o")
))),
/* Plugin Manager Modal — scan VST3/AU (spec desktop 2026-08-08) */
vstScanOpen && /*#__PURE__*/React.createElement("div", {
className: "fixed inset-0 bg-black/80 backdrop-blur-sm z-[120] flex items-center justify-center p-4",
onMouseDown: e => e.stopPropagation(),
onClick: e => e.stopPropagation()
}, /*#__PURE__*/React.createElement("div", { className: "w-full max-w-2xl bg-[#1e1e24] border border-zinc-700 rounded-2xl p-5 shadow-2xl space-y-4 text-zinc-200" },
/*#__PURE__*/React.createElement("div", { className: "flex items-center justify-between border-b border-zinc-700 pb-3" },
/*#__PURE__*/React.createElement("h2", { className: "text-sm font-bold text-purple-400 flex items-center gap-2" }, "\uD83D\uDD0C Plugin Manager (VST3 / AU)"),
/*#__PURE__*/React.createElement("button", { onClick: () => setVstScanOpen(false), className: "text-zinc-400 hover:text-white text-lg" }, "\u2715")
),
/*#__PURE__*/React.createElement("div", { className: "space-y-2 max-h-[50vh] overflow-y-auto" },
vstPlugins.length === 0 && !vstScanning && /*#__PURE__*/React.createElement("p", { className: "text-xs text-zinc-500 text-center py-6" }, "Ch\u01B0a qu\u00E9t — nh\u1EADp \u0111\u01B0\u1EDDng d\u1EABn th\u01B0 m\u1EE5c ch\u1EE9a plugin (m\u1EB7c \u0111\u1ECBnh /opt/daw_engine/vst3) r\u1ED3i b\u1EA5m Scan."),
vstPlugins.map((plg) => /*#__PURE__*/React.createElement("div", {
key: plg.id,
onClick: () => {
setTrackInstrumentWithUndo(instrumentDropdownTrackId, plg.id, plg.name + " (" + plg.format + ")", undefined, undefined, { plugin_name: plg.name, format: plg.format, plugin_path: plg.path });
setVstScanOpen(false);
setVstPlugins([]);
},
className: "bg-[#22222a] hover:bg-[#2c2c36] border border-zinc-700/60 p-3 rounded-xl flex items-center justify-between cursor-pointer transition-all"
}, /*#__PURE__*/React.createElement("div", { className: "flex items-center gap-3" },
/*#__PURE__*/React.createElement("div", { className: "w-8 h-8 rounded-lg bg-purple-950/80 border border-purple-800 flex items-center justify-center text-purple-400 text-xs" }, "\uD83C\uDFB5"),
/*#__PURE__*/React.createElement("div", null,
/*#__PURE__*/React.createElement("div", { className: "text-xs font-bold text-white" }, plg.name),
/*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-400 font-mono" }, plg.path)
)
), /*#__PURE__*/React.createElement("span", { className: "text-[10px] font-mono font-bold bg-purple-950 text-purple-300 border border-purple-800 px-2 py-0.5 rounded-full" }, plg.format)))
),
/*#__PURE__*/React.createElement("div", { className: "pt-3 border-t border-zinc-700/80 space-y-1" },
/*#__PURE__*/React.createElement("label", { className: "block text-[10px] font-bold text-zinc-400 uppercase" }, "ADD VST DIRECTORY"),
/*#__PURE__*/React.createElement("div", { className: "flex gap-2" },
/*#__PURE__*/React.createElement("input", {
type: "text", value: vstScanPath,
onChange: e => setVstScanPath(e.target.value),
className: "flex-1 bg-[#141418] border border-zinc-700 rounded-lg px-3 py-1.5 text-xs text-zinc-200 font-mono outline-none focus:border-purple-500"
}),
/*#__PURE__*/React.createElement("button", {
onClick: async () => {
setVstScanning(true);
setVstScanError(null);
try {
const resp = await fetch('/api/v1/desktop/plugins/scan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ custom_directories: [vstScanPath], scan_formats: ['VST3', 'AU'], force_rescan: true })
});
const data = await resp.json();
if (data.status === 'success') setVstPlugins(data.plugins || []);
else setVstScanError(data.detail || 'Scan thất bại');
} catch (err) {
setVstScanError('Lỗi kết nối: ' + err.message);
} finally {
setVstScanning(false);
}
},
disabled: vstScanning,
className: "px-5 py-1.5 bg-purple-600 hover:bg-purple-500 disabled:bg-zinc-700 text-white rounded-lg text-xs font-bold transition-all flex items-center gap-2"
}, vstScanning ? "Scanning..." : "Scan")
),
vstScanError && /*#__PURE__*/React.createElement("p", { className: "text-[10px] text-red-400" }, vstScanError),
/*#__PURE__*/React.createElement("p", { className: "text-[10px] text-zinc-600" }, "Windows: C:\\Program Files\\Common Files\\VST3 \u00B7 macOS: /Library/Audio/Plug-Ins/Components \u00B7 Linux: /opt/daw_engine/vst3, ~/.vst3")
)
))
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(/*#__PURE__*/React.createElement(App, null));
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -24,7 +24,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608070950" defer></script>
<script src="/static/js/app.precompiled.js?v=202608080820" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {