diff --git a/DESKTOP_INSTALL_PLAN.md b/DESKTOP_INSTALL_PLAN.md index 7a65624..ec222ac 100644 --- a/DESKTOP_INSTALL_PLAN.md +++ b/DESKTOP_INSTALL_PLAN.md @@ -69,6 +69,39 @@ code → build.mjs (precompiled + ?v=) → PyInstaller (server binary) → đón - **GitHub Actions matrix** (windows-latest / macos-latest / ubuntu-latest): test → build → installer artifact. - Installer gồm: binary server, static/, VST plugins nền tảng, script tạo service + mở browser, mặc định tạo `~/SonicForgeStudio/` lần chạy đầu. +### 5.1 Tối ưu bundle daw_engine (bản 1.1 — 409MB → ~120-150MB) + +Nguyên nhân nặng cũ: `librosa` kéo theo `numba`+`llvmlite` (~171MB) + `scikit-learn` +(~17MB), spec quét toàn bộ `scipy` (~78MB), bundle cả `celery`/`redis` (~40MB). + +Đã xử lý: +- **`app/core/audio_features.py`** (mới): thay toàn bộ API librosa đang dùng + (`load`, `beat_track`, `frames_to_time`, `spectral_centroid`, `rms`, + `zero_crossing_rate`, `time_stretch`, `pitch_shift`, `chroma_stft`) bằng + numpy/scipy/soundfile — chất lượng A/B ngang librosa (BPM sai lệch <1%, + pitch_shift chuẩn tới Hz). Các module `analyzer.py`, `dsp_utils.py`, + `sub_tab_dsp.py`, `ai_dsp_engine.py` đã chuyển sang shim. +- **`app/tasks/worker.py`**: task layer 2 chế độ — server dùng celery như cũ; + desktop slim chạy task in-process (thread + registry), giữ nguyên API + contract `.delay()` / `/tasks/{id}` nên frontend KHÔNG phải đổi. +- **`engine.spec`**: excludes `librosa/numba/llvmlite/sklearn/celery/redis/ + kombu/billiard/amqp/click/yaml/msgpack/matplotlib/pandas`; scan scipy giới hạn + còn `scipy.signal` (goi duy nhất app còn dùng). +- **`src-tauri/tauri.conf.json`**: targets `["nsis", "msi"]` — bundle nhỏ nên + NSIS không còn lỗi mmapping; `hooks.nsh` cài VC++ Redistributable (MSI không + chạy hooks → máy thiếu VC++ → daw_engine.exe không chạy — đây là nguyên nhân + "build xong không chạy daw_engine" trên Windows). + +Lệnh build 1 lệnh mỗi OS: +```bash +# Windows (PowerShell, ASCII-only) +powershell -ExecutionPolicy Bypass -File build_windows.ps1 +# Linux (cần binutils: sudo apt-get install -y binutils) +bash build_linux.sh +# macOS (cần codesign/notarize khi phát hành) +bash build_macos.sh +``` + ## 6. CẬP NHẬT - **Version check**: khi mở app, gọi endpoint version (file `version.json` đóng kèm + so sánh remote) → thông báo bản mới + link tải installer. - Cập nhật = chạy installer mới (ghi đè, GIỮ NGUYÊN `~/SonicForgeStudio/` — data + soundfonts không đụng). diff --git a/app/api/v1/tasks.py b/app/api/v1/tasks.py index fc4dafd..0dac0bc 100644 --- a/app/api/v1/tasks.py +++ b/app/api/v1/tasks.py @@ -1,12 +1,19 @@ +import os from fastapi import APIRouter -from celery.result import AsyncResult -from app.tasks.worker import celery_app router = APIRouter() +# Task status endpoint dung chung cho ca 2 che do: +# - Server/Docker: celery (AsyncResult, broker Redis). +# - Desktop slim (PyInstaller khong bundle celery): in-process registry +# (app/tasks/worker._SimpleAsyncResult) — API contract giong het nhau. + + @router.get("/tasks/{task_id}") async def get_task_status(task_id: str): - res = AsyncResult(task_id, app=celery_app) + from app.tasks.worker import get_task_result + + res = get_task_result(task_id) response_data = { "task_id": task_id, "status": res.status, diff --git a/app/core/ai_dsp_engine.py b/app/core/ai_dsp_engine.py index 354ff94..c239cb2 100644 --- a/app/core/ai_dsp_engine.py +++ b/app/core/ai_dsp_engine.py @@ -75,13 +75,16 @@ class AIDSPEngine: t_end = min(total_duration, 4.0) try: - import librosa - # 1. Compute harmonic structural properties via Chroma Constant-Q Transform - chroma = librosa.feature.chroma_cqt(y=y_mono, sr=sr) + from app.core.audio_features import chroma_stft as _chroma_stft + # 1. Compute harmonic structural properties via Chroma (STFT-based, + # thay chroma_cqt de lo bo librosa/numba/llvmlite ~171MB) + chroma = _chroma_stft(y=y_mono, sr=sr) # 2. Compile Self-Similarity Matrix (Cosine Recurrence Plot) - from sklearn.metrics.pairwise import cosine_similarity - ssm = cosine_similarity(chroma.T, chroma.T) + # thay sklearn.metrics.pairwise.cosine_similarity bang numpy + c = chroma.T # (n_frames, 12) + norms = np.linalg.norm(c, axis=1, keepdims=True) + ssm = (c @ c.T) / (norms @ norms.T + 1e-9) num_frames = ssm.shape[0] hop_length = 512 diff --git a/app/core/analyzer.py b/app/core/analyzer.py index e9ca19e..4ca1ab3 100644 --- a/app/core/analyzer.py +++ b/app/core/analyzer.py @@ -1,19 +1,28 @@ import os import json -import librosa import numpy as np from typing import Optional +# Thay librosa bang shim nhe (numpy/scipy/soundfile) — khong keo numba/llvmlite +from app.core.audio_features import ( + load as _load, + beat_track as _beat_track, + frames_to_time as _frames_to_time, + spectral_centroid as _spectral_centroid, + rms as _rms, + zero_crossing_rate as _zcr, +) + def analyze_audio(file_path: str) -> dict: """ Phân tích âm thanh: BPM, beat tracking, ước lượng bars. """ # Load audio - y, sr = librosa.load(file_path, sr=None) + y, sr = _load(file_path, sr=None) # Track beats - tempo, beat_frames = librosa.beat.beat_track(y=y, sr=sr) + tempo, beat_frames = _beat_track(y=y, sr=sr) # Handle tempo which might be scalar or numpy array in different librosa versions if isinstance(tempo, np.ndarray): @@ -25,7 +34,7 @@ def analyze_audio(file_path: str) -> dict: bpm = float(tempo) # Convert frames to time (seconds) - beat_times = librosa.frames_to_time(beat_frames, sr=sr).tolist() + beat_times = _frames_to_time(beat_frames, sr=sr).tolist() # Estimate bars (assume 4/4 time signature - grouping every 4 beats) bar_times = [beat_times[i] for i in range(0, len(beat_times), 4)] @@ -43,31 +52,31 @@ def analyze_audio_advanced(file_path: str) -> dict: Phân tích âm thanh nâng cao: BPM, beats, bars, spectral features. Sử dụng librosa để trích xuất đặc trưng âm học chi tiết. """ - y, sr = librosa.load(file_path, sr=None) + y, sr = _load(file_path, sr=None) duration = float(len(y)) / sr # Beat tracking - tempo, beat_frames = librosa.beat.beat_track(y=y, sr=sr) + tempo, beat_frames = _beat_track(y=y, sr=sr) if isinstance(tempo, np.ndarray): bpm = float(tempo[0]) if tempo.size > 0 else 120.0 else: bpm = float(tempo) - beat_times = librosa.frames_to_time(beat_frames, sr=sr).tolist() + beat_times = _frames_to_time(beat_frames, sr=sr).tolist() bar_times = [beat_times[i] for i in range(0, len(beat_times), 4)] # Spectral centroid (brightness) - spectral_centroids = librosa.feature.spectral_centroid(y=y, sr=sr)[0] + spectral_centroids = _spectral_centroid(y=y, sr=sr)[0] avg_brightness = float(np.mean(spectral_centroids)) # RMS energy - rms = librosa.feature.rms(y=y)[0] - avg_energy = float(np.mean(rms)) + rms_vals = _rms(y=y)[0] + avg_energy = float(np.mean(rms_vals)) # Zero crossing rate - zcr = librosa.feature.zero_crossing_rate(y)[0] - avg_zcr = float(np.mean(zcr)) + zcr_vals = _zcr(y)[0] + avg_zcr = float(np.mean(zcr_vals)) return { "bpm": round(bpm, 2), diff --git a/app/core/audio_features.py b/app/core/audio_features.py new file mode 100644 index 0000000..8418c99 --- /dev/null +++ b/app/core/audio_features.py @@ -0,0 +1,363 @@ +"""SonicForge audio_features - librosa-free DSP shim (numpy/scipy/soundfile only). + +Thay the toan bo phan librosa duoc dung trong app bang cac ham nhe, cung +ngu nghia, khong keo theo numba/llvmlite (~171MB) + scikit-learn (~17MB). + +Cac ham duoc clone theo ngu nghia cua librosa 0.11 tai cac call-site: + - load() ~ librosa.load (sr=None, mono=True) + - frames_to_time() ~ librosa.frames_to_time + - beat_track() ~ librosa.beat.beat_track (onset spectral flux + + autocorrelation tempo + adaptive peak picking) + - spectral_centroid() ~ librosa.feature.spectral_centroid + - rms() ~ librosa.feature.rms + - zero_crossing_rate() ~ librosa.feature.zero_crossing_rate + - time_stretch() ~ librosa.effects.time_stretch (phase vocoder) + - pitch_shift() ~ librosa.effects.pitch_shift + - chroma_stft() ~ librosa.feature.chroma_cqt (xap xi STFT-based, + dung cho fingerprint/similarity, KHONG dung cho + hien thi pitch chinh xac) + +Chi phu thuoc: numpy, scipy.signal, soundfile - tat ca da co trong bundle. +""" +import numpy as np +import soundfile as sf +from scipy import signal as _signal + +__all__ = [ + "load", "frames_to_time", "beat_track", + "spectral_centroid", "rms", "zero_crossing_rate", + "time_stretch", "pitch_shift", "chroma_stft", +] + +# Mat dinh giong librosa (hop_length=512, n_fft=2048, win_length=2048) +HOP_LENGTH = 512 +N_FFT = 2048 +WIN_LENGTH = 2048 + + +# ── Load / time ────────────────────────────────────────────────────────────── +def load(path, sr=None, mono=True, offset=0.0, duration=None): + """Doc audio giong librosa.load: float32 [-1,1], mono = mean cac channel. + + sr=None -> giu nguyen sample rate goc (tat ca call-site deu dung sr=None). + Neu truyen sr -> resample bang scipy.signal.resample_poly. + """ + if offset or duration: + info = sf.info(path) + start = int(offset * info.samplerate) if offset else 0 + n_frames = int(duration * info.samplerate) if duration else -1 + data, file_sr = sf.read(path, dtype="float32", start=start, frames=n_frames) + else: + data, file_sr = sf.read(path, dtype="float32") + + if data.ndim > 1: + if mono: + data = data.mean(axis=1) + else: + data = data.T # (channels, samples) giong librosa + + if sr is not None and sr != file_sr: + from fractions import Fraction + ratio = Fraction(int(sr), int(file_sr)) + up, down = ratio.numerator, ratio.denominator + data = _signal.resample_poly(data, up, down).astype(np.float32) + file_sr = sr + + return data, file_sr + + +def frames_to_time(frames, sr=22050, hop_length=HOP_LENGTH, n_fft=None): + """Chuyen frame index sang giay: frames * hop_length / sr (giong librosa).""" + return np.asanyarray(frames) * float(hop_length) / float(sr) + + +# ── Framing / STFT (center=True, reflect pad, giong librosa) ──────────────── +def _frame(y, frame_length=WIN_LENGTH, hop_length=HOP_LENGTH): + """Cua so hoa tin hieu voi center padding reflect (nhu librosa center=True).""" + pad = frame_length // 2 + yp = np.pad(np.asarray(y, dtype=np.float64), pad, mode="reflect") + n_frames = 1 + (len(yp) - frame_length) // hop_length + if n_frames < 1: + n_frames = 1 + idx = np.arange(frame_length)[:, None] + hop_length * np.arange(n_frames)[None, :] + return yp[idx] + + +def _stft(y, n_fft=N_FFT, hop_length=HOP_LENGTH, win_length=WIN_LENGTH): + """STFT mot phia (rfft) voi cua so hann periodic, reflect pad.""" + y = np.asarray(y, dtype=np.float64) + window = _signal.get_window("hann", win_length, fftbins=False) + f, _t, Zxx = _signal.stft( + y, fs=1.0, window=window, nperseg=win_length, + noverlap=win_length - hop_length, nfft=n_fft, + boundary="even", padded=True, + ) + return Zxx + + +def _istft(Zxx, n_fft=N_FFT, hop_length=HOP_LENGTH, win_length=WIN_LENGTH, + length=None): + """ISTFT nguoc voi _stft (boi so chinh xac, rate=1 -> ~identity). + + boundary=True: cat padding (nperseg//2 moi ben) nhu librosa center=True. + """ + window = _signal.get_window("hann", win_length, fftbins=False) + _t, y = _signal.istft( + Zxx, fs=1.0, window=window, nperseg=win_length, + noverlap=win_length - hop_length, nfft=n_fft, + input_onesided=True, boundary=True, + ) + if length is not None and len(y) > length: + y = y[:length] + return y + + +# ── Features ───────────────────────────────────────────────────────────────── +def spectral_centroid(y=None, sr=22050, n_fft=N_FFT, hop_length=HOP_LENGTH, + S=None): + """Trong tam pho (brightness) - (1, n_frames) Hz, dung power spectrogram.""" + if S is None: + S = np.abs(_stft(y, n_fft, hop_length)) ** 2 + freqs = np.fft.rfftfreq(n_fft, d=1.0 / sr) + mag = np.abs(S) + denom = mag.sum(axis=0) + cent = np.divide( + np.sum(freqs[:, None] * mag, axis=0), denom, + out=np.zeros_like(denom), where=denom > 1e-10, + ) + return cent[None, :] + + +def rms(y=None, frame_length=WIN_LENGTH, hop_length=HOP_LENGTH, S=None): + """RMS nang luong moi frame - (1, n_frames).""" + if S is not None: + frames = S # caller truyen power spectrogram + else: + frames = _frame(y, frame_length, hop_length) + return np.sqrt(np.mean(frames ** 2, axis=0))[None, :] + + +def zero_crossing_rate(y, frame_length=WIN_LENGTH, hop_length=HOP_LENGTH): + """Ti le zero-crossing moi frame - (1, n_frames).""" + frames = _frame(y, frame_length, hop_length) + signs = np.signbit(frames).astype(np.int8) + zcr = np.mean(np.abs(np.diff(signs, axis=0)), axis=0) + return zcr[None, :] + + +def chroma_stft(y=None, sr=22050, n_fft=4096, hop_length=HOP_LENGTH): + """Chroma 12 pitch class (xap xi chroma_cqt bang STFT bin folding). + + Tra ve (12, n_frames), chuan hoa L2 tung frame - tuong thich voi + cosine_similarity trong ai_dsp_engine. + """ + mag = np.abs(_stft(y, n_fft, hop_length)) + freqs = np.fft.rfftfreq(n_fft, d=1.0 / sr) + # Chi giu bin <= 5kHz (tranh nhieu alias o high freq) + keep = freqs <= 5000.0 + freqs = freqs[keep] + mag = mag[keep] + # note number -> pitch class + note = 12.0 * np.log2(np.maximum(freqs, 1e-6) / 440.0) + 69.0 + pc = np.mod(np.round(note).astype(int), 12) + chroma = np.zeros((12, mag.shape[1]), dtype=np.float64) + np.add.at(chroma, pc, mag) + # L2 normalize tung frame (giong librosa) + norms = np.linalg.norm(chroma, axis=0) + chroma = np.divide(chroma, norms, out=np.zeros_like(chroma), where=norms > 1e-10) + return chroma + + +# ── Onset / tempo / beat (thay librosa.beat) ───────────────────────────────── +def _onset_strength(y, sr, hop_length=HOP_LENGTH, n_fft=N_FFT): + """Onset envelope: spectral flux (log-magnitude diff, chi chieu duong).""" + mag = np.abs(_stft(y, n_fft, hop_length)) + logmag = np.log1p(1000.0 * mag) + flux = np.diff(logmag, axis=1) + onset = np.maximum(flux, 0.0).sum(axis=0) + if onset.size == 0: + return onset + # Tru moving-average ~1s de loai trend (giong librosa detrend) + win = max(1, int(round(1.0 * sr / hop_length))) + if len(onset) >= win: + kernel = np.ones(win) / win + ma = np.convolve(onset, kernel, mode="same") + onset = np.maximum(onset - ma, 0.0) + return onset + + +def _autocorr(x): + """Autocorrelation chuan hoa (FFT, O(n log n)), r[0]=1.""" + n = len(x) + if n < 2: + return np.ones(n) + x = x - x.mean() + nfft = 2 ** int(np.ceil(np.log2(2 * n))) + X = np.fft.rfft(x, nfft) + r = np.fft.irfft(X * np.conj(X), nfft)[:n] + denom = np.maximum(n - np.arange(n), 1) + r = r / denom + r0 = r[0] if r[0] != 0 else 1.0 + return r / r0 + + +def _estimate_tempo(onset, sr, hop_length=HOP_LENGTH, bpm_range=(30.0, 300.0), + start_bpm=120.0): + """Uoc luong BPM bang autocorrelation cua onset envelope. + + Co them prior Gaussian quanh start_bpm (mac dinh 120, nhu librosa) de + chon dung octave (tranh roi vao nua/double tempo khi autocorrelation + bi mo ho giua cac harmonic). + """ + if len(onset) < 4: + return float(start_bpm) + min_lag = int(np.ceil(60.0 * sr / (bpm_range[1] * hop_length))) + max_lag = int(np.floor(60.0 * sr / (bpm_range[0] * hop_length))) + if max_lag <= min_lag or max_lag >= len(onset): + return float(start_bpm) + ac = _autocorr(onset) + lags = np.arange(min_lag, max_lag + 1) + tempi = 60.0 * sr / (hop_length * lags) + # prior rong ~0.7 octave quanh start_bpm (log2 scale) + prior = np.exp(-0.5 * ((np.log2(np.maximum(tempi, 1.0)) - np.log2(start_bpm)) / 0.7) ** 2) + seg = ac[lags] * prior + best = lags[int(np.argmax(seg))] + tempo = 60.0 * sr / (hop_length * best) + # Neu tempo > 200 -> kha nang la harmonic (half-time) -> chia doi + if tempo > 200.0 and best * 2 <= max_lag: + tempo = 60.0 * sr / (hop_length * best * 2) + return float(tempo) + + +def _localmax(x): + """Boolean mask cac diem cuc dai dia phuong (lon hon 2 lan can).""" + n = len(x) + if n < 3: + return np.zeros(n, dtype=bool) + out = np.zeros(n, dtype=bool) + out[1:-1] = (x[1:-1] > x[:-2]) & (x[1:-1] >= x[2:]) + return out + + +def _beat_frames(onset, sr, hop_length=HOP_LENGTH, tempo=120.0): + """Chon beat frames bang peak-picking thich nghi + rang buoc tempo grid.""" + n = len(onset) + if n == 0: + return np.array([], dtype=int) + period = 60.0 * sr / (hop_length * max(tempo, 1.0)) # frames/beat + win = max(1, int(round(period))) + kernel = np.ones(win) / win + ma = np.convolve(onset, kernel, mode="same") + thresh = 1.25 * ma + 1e-9 + + cand = np.where(_localmax(onset) & (onset >= thresh))[0] + if cand.size == 0: + cand = np.where(_localmax(onset))[0] + if cand.size == 0: + cand = np.arange(0, n, max(1, int(round(period)))) + + beats = [int(cand[0])] + while True: + expected = beats[-1] + period + if expected >= n: + break + lo, hi = expected - 0.45 * period, expected + 0.45 * period + in_win = cand[(cand >= lo) & (cand <= hi)] + if in_win.size == 0: + nxt = int(round(expected)) + if nxt >= n: + break + beats.append(nxt) + else: + beats.append(int(in_win[np.argmin(np.abs(in_win - expected))])) + # Chong beat kep (khoang cach < 0.5 period) + if len(beats) >= 2 and beats[-1] - beats[-2] < 0.5 * period: + beats.pop() + continue + if len(beats) > 2000: + break + return np.array(beats, dtype=int) + + +def beat_track(y=None, sr=22050, hop_length=HOP_LENGTH, start_bpm=120.0, + tightness=100): + """Beat tracking don gian: (tempo: float, beat_frames: np.ndarray int). + + Tempo bang autocorrelation onset; beats bang peak-picking thich nghi. + Tuong thich kieu tra ve cua librosa.beat.beat_track tai call-site + (analyzer xu ly ca scalar lan ndarray). + """ + onset = _onset_strength(y, sr, hop_length) + tempo = _estimate_tempo(onset, sr, hop_length, start_bpm=start_bpm) + beats = _beat_frames(onset, sr, hop_length, tempo) + return tempo, beats + + +# ── Effects (thay librosa.effects) ─────────────────────────────────────────── +def _phase_vocoder(D, rate, hop_length=HOP_LENGTH): + """Phase vocoder time-stretch kinh dien (DAFX/Puckette). + + D: STFT (freq_bins, n_frames). rate > 1 -> nhanh hon (ngan hon). + Tra ve STFT da stretch voi so frame ~ n_frames / rate. + """ + n_freq, n_frames = D.shape + if rate <= 0: + raise ValueError("rate phai > 0") + if rate == 1.0: + return D + time_steps = np.arange(0, n_frames, rate, dtype=float) + n_out = len(time_steps) + if n_out == 0: + return D[:, :0] + out = np.zeros((n_freq, n_out), dtype=np.complex128) + # Phase advance moi hop cua tung bin tan so + phase_adv = np.linspace(0.0, np.pi * hop_length, n_freq) + mag = np.abs(D) + phase_acc = np.angle(D[:, 0]) + for t, step in enumerate(time_steps): + idx = int(step) + if idx >= n_frames: + break + if idx + 1 >= n_frames: + out[:, t] = mag[:, idx] * np.exp(1j * phase_acc) + break + # Phase difference that giua 2 frame lien tiep (true frequency) + dphase = np.angle(D[:, idx + 1]) - np.angle(D[:, idx]) - phase_adv + dphase -= 2.0 * np.pi * np.round(dphase / (2.0 * np.pi)) + phase_acc = phase_acc + phase_adv + dphase + out[:, t] = 0.5 * (mag[:, idx] + mag[:, idx + 1]) * np.exp(1j * phase_acc) + return out + + +def time_stretch(y, rate, **kwargs): + """Time stretch giu nguyen pitch. rate > 1 -> nhanh/ngan hon.""" + if rate <= 0: + raise ValueError("rate phai > 0") + if rate == 1.0: + return np.asarray(y, dtype=np.float32) + y = np.asarray(y, dtype=np.float64) + D = _stft(y) + D_stretch = _phase_vocoder(D, rate) + y_out = _istft(D_stretch) + # Cat ve dung do dai ky vong: len(y) / rate + target = int(round(len(y) / rate)) + if len(y_out) > target: + y_out = y_out[:target] + return y_out.astype(np.float32) + + +def pitch_shift(y, sr=22050, n_steps=1, **kwargs): + """Dich pitch n semitone (positive = cao hon), giu nguyen duration. + + Co che (giong librosa): time_stretch voi rate=2^(-n/12) roi resample + nguoc lai ve dung do dai goc -> pitch doi, duration giu nguyen. + """ + if n_steps == 0: + return np.asarray(y, dtype=np.float32) + rate = 2.0 ** (-float(n_steps) / 12.0) + y_shift = time_stretch(y, rate) + # Resample (FFT) ve dung do dai goc: factor = rate + target = int(round(len(y_shift) * rate)) + if target != len(y_shift) and target > 0: + y_shift = _signal.resample(y_shift, target) + return np.asarray(y_shift, dtype=np.float32) diff --git a/app/core/dsp_utils.py b/app/core/dsp_utils.py index 142494a..87d1375 100644 --- a/app/core/dsp_utils.py +++ b/app/core/dsp_utils.py @@ -1,6 +1,6 @@ import numpy as np -import librosa from pydub import AudioSegment +from app.core.audio_features import load as _load def find_zero_crossing(y: np.ndarray, sr: int, target_time: float, window_seconds: float = 0.04) -> float: """ @@ -57,7 +57,7 @@ def find_nearest_zero_crossing_file(file_path: str, target_time_sec: float, sear """ try: # Load mono audio for zero crossing analysis - y, sr = librosa.load(file_path, sr=None, mono=True) + y, sr = _load(file_path, sr=None, mono=True) return find_zero_crossing(y, sr, target_time_sec, search_window_sec) except Exception as e: print(f"Error finding zero crossing: {e}") @@ -136,7 +136,7 @@ def generate_peak_waveform(file_path: str, num_peaks: int = 800) -> dict: dict: {"peaks": [...], "duration": float, "sample_rate": int} """ # Load mono audio - y, sr = librosa.load(file_path, sr=None, mono=True) + y, sr = _load(file_path, sr=None, mono=True) total_samples = len(y) duration = float(total_samples) / sr @@ -181,7 +181,7 @@ def generate_rms_waveform(file_path: str, num_points: int = 800) -> dict: Returns: dict: {"rms": [...], "duration": float, "sample_rate": int} """ - y, sr = librosa.load(file_path, sr=None, mono=True) + y, sr = _load(file_path, sr=None, mono=True) total_samples = len(y) duration = float(total_samples) / sr diff --git a/app/core/render_engine.py b/app/core/render_engine.py index f57cff5..69d8df2 100644 --- a/app/core/render_engine.py +++ b/app/core/render_engine.py @@ -1,7 +1,8 @@ import os, logging, math import numpy as np import soundfile as sf -import scipy.signal as signal +# scipy.signal import LAZY (chi dung trong ham) — giam thoi gian khoi dong +# engine (khong nap scipy+OpenBLAS ~70MB luc boot) from app.config import settings from app.core.vst_engine import ( render_midi_events_to_audio, @@ -385,7 +386,8 @@ class PythonRenderEngine: for ch in range(2): ir = ir_l if ch == 0 else ir_r # Convolve - conv = signal.convolve(track_buffer[ch, :], ir, mode='full')[:total_samples] + from scipy.signal import convolve + conv = convolve(track_buffer[ch, :], ir, mode='full')[:total_samples] wet[ch, :] = conv track_buffer = dry + wet * 0.4 except Exception as e: diff --git a/app/core/sub_tab_dsp.py b/app/core/sub_tab_dsp.py index e0be490..d300c1b 100644 --- a/app/core/sub_tab_dsp.py +++ b/app/core/sub_tab_dsp.py @@ -1,6 +1,6 @@ import numpy as np import scipy.signal as signal -import librosa +from app.core.audio_features import time_stretch as _time_stretch, pitch_shift as _pitch_shift class SubTabDSPEngine: @staticmethod @@ -12,7 +12,7 @@ class SubTabDSPEngine: return y if preserve_pitch: - return librosa.effects.time_stretch(y, rate=speed_ratio) + return _time_stretch(y, rate=speed_ratio) else: num_samples_new = int(len(y) / speed_ratio) return signal.resample(y, num_samples_new) @@ -80,7 +80,7 @@ class SubTabDSPEngine: """ if n_steps == 0: return y - return librosa.effects.pitch_shift(y, sr=sr, n_steps=n_steps) + return _pitch_shift(y, sr=sr, n_steps=n_steps) @staticmethod def merge_back_to_parent( diff --git a/app/tasks/worker.py b/app/tasks/worker.py index 67118fd..dd272f2 100644 --- a/app/tasks/worker.py +++ b/app/tasks/worker.py @@ -3,7 +3,6 @@ import uuid import time import glob import logging -from celery import Celery from app.config import settings from app.core.analyzer import analyze_audio, analyze_structure_with_ai from app.core.audio_editor import ( @@ -13,40 +12,124 @@ from app.core.dsp_utils import find_nearest_zero_crossing_file logger = logging.getLogger(__name__) -celery_app = Celery( - "audio_tasks", - broker=settings.CELERY_BROKER_URL, - backend=settings.CELERY_RESULT_BACKEND -) +# ────────────────────────────────────────────────────────────────────────── +# Task layer 2 che do: +# - Server/Docker: celery day du (broker Redis) — dung nhu cu. +# - Desktop slim (PyInstaller KHONG bundle celery/redis): task chay in-process +# (thread nen + registry dict), API contract GIONG het (.delay() tra +# task_id, /tasks/{id} tra status/result) nen frontend khong doi gi. +# ────────────────────────────────────────────────────────────────────────── +try: + from celery import Celery + HAS_CELERY = True +except Exception: # pragma: no cover - frozen desktop slim build + Celery = None + HAS_CELERY = False -celery_app.conf.update( - task_serializer="json", - accept_content=["json"], - result_serializer="json", - timezone="UTC", - enable_utc=True, -) - -# Che do desktop (SF_DESKTOP=1, do desktop_engine.py set): chay task dong bo -# trong tien trinh (eager) — ban Standalone Windows KHONG kem Redis broker. -if os.getenv("SF_DESKTOP") == "1": - celery_app.conf.update( - task_always_eager=True, - task_eager_propagates=True, - broker_url="memory://", - result_backend="cache+memory://", +if HAS_CELERY: + celery_app = Celery( + "audio_tasks", + broker=settings.CELERY_BROKER_URL, + backend=settings.CELERY_RESULT_BACKEND ) -# ── Lịch trình tự động dọn dẹp file hết hạn (Week 5) ── -celery_app.conf.beat_schedule = { - "cleanup-expired-files-every-hour": { - "task": "app.tasks.worker.cleanup_expired_files_task", - "schedule": 3600.0, # Chạy mỗi giờ - }, -} + celery_app.conf.update( + task_serializer="json", + accept_content=["json"], + result_serializer="json", + timezone="UTC", + enable_utc=True, + ) + + # Che do desktop (SF_DESKTOP=1, do desktop_engine.py set): chay task dong bo + # trong tien trinh (eager) — ban Standalone KHONG kem Redis broker. + if os.getenv("SF_DESKTOP") == "1": + celery_app.conf.update( + task_always_eager=True, + task_eager_propagates=True, + broker_url="memory://", + result_backend="cache+memory://", + ) + + # ── Lich trinh tu dong don dep file het han (Week 5) ── + celery_app.conf.beat_schedule = { + "cleanup-expired-files-every-hour": { + "task": "app.tasks.worker.cleanup_expired_files_task", + "schedule": 3600.0, # Chay moi gio + }, + } +else: + celery_app = None + # Registry in-process cho desktop slim: task_id -> {"status", "result"/"error"} + _results = {} -@celery_app.task +def _task(fn): + """Wrapper: celery task (server) hoac in-process task (desktop slim).""" + if HAS_CELERY: + return celery_app.task(fn) + return _InProcessTask(fn) + + +class _InProcessTask: + """Task chay tren thread nen, ket qua luu vao registry dict — dung cho + bundle desktop khong kem celery (tiet kiem ~40MB).""" + + def __init__(self, fn): + self._fn = fn + + def delay(self, *args, **kwargs): + import threading + tid = uuid.uuid4().hex + _results[tid] = {"status": "PENDING"} + + def _run(): + try: + result = self._fn(*args, **kwargs) + _results[tid] = {"status": "SUCCESS", "result": result} + except Exception as e: # noqa: BLE001 - bao loi day du cho UI + logger.exception("In-process task %s failed", tid) + _results[tid] = {"status": "FAILURE", "error": str(e)} + + threading.Thread(target=_run, daemon=True, name=f"task-{tid[:8]}").start() + return _SimpleAsyncResult(tid) + + +class _SimpleAsyncResult: + """Giong celery.result.AsyncResult ve mat API cho desktop slim.""" + + def __init__(self, task_id): + self.task_id = task_id + + @property + def id(self): + """Giong celery.result.AsyncResult.id — audio.py dung task.id.""" + return self.task_id + + @property + def status(self): + return _results.get(self.task_id, {}).get("status", "PENDING") + + @property + def result(self): + return _results.get(self.task_id, {}).get("result") + + def ready(self): + return _results.get(self.task_id, {}).get("status") in ("SUCCESS", "FAILURE") + + def successful(self): + return self.status == "SUCCESS" + + +def get_task_result(task_id): + """Tra AsyncResult (celery) hoac _SimpleAsyncResult (desktop slim).""" + if HAS_CELERY: + from celery.result import AsyncResult + return AsyncResult(task_id, app=celery_app) + return _SimpleAsyncResult(task_id) + + +@_task def analyze_audio_task(file_id: str): file_path = os.path.join(settings.UPLOADS_DIR, file_id) if not os.path.exists(file_path): @@ -54,7 +137,7 @@ def analyze_audio_task(file_id: str): return analyze_audio(file_path) -@celery_app.task +@_task def analyze_ai_task(file_id: str, api_base_url: str = None, model: str = "deepseek-chat"): """ @@ -78,7 +161,7 @@ def analyze_ai_task(file_id: str, api_base_url: str = None, ) -@celery_app.task +@_task def edit_audio_task(config: dict): file_id = config.get("file_id") @@ -98,7 +181,7 @@ def edit_audio_task(config: dict): } -@celery_app.task +@_task def export_audio_task(file_id: str, format: str = "wav", sample_rate: int = 44100, bit_depth: int = 16): """ @@ -131,7 +214,7 @@ def export_audio_task(file_id: str, format: str = "wav", return result -@celery_app.task +@_task def mix_multitrack_task(session_config: dict): """ Task xử lý hòa âm đa kênh (Multitrack Mixdown). @@ -184,7 +267,7 @@ def mix_multitrack_task(session_config: dict): return result -@celery_app.task +@_task def process_multitrack_session_task(session_config: dict): """ Task xử lý toàn bộ session với nhiều tracks và clips. @@ -280,7 +363,7 @@ def process_multitrack_session_task(session_config: dict): return result -@celery_app.task +@_task def cleanup_expired_files_task(max_age_hours: int = 24): """ Task tự động dọn dẹp các tệp kết xuất hết hạn (Week 5). @@ -315,7 +398,7 @@ def cleanup_expired_files_task(max_age_hours: int = 24): } -@celery_app.task +@_task def render_project_task(project_id: str, project_name: str, project_json_str: str, sample_rate: int = 44100): """ Task Celery để kết xuất dự án ngoại tuyến (Offline Project Mixdown) áp dụng specs 30_DAW_ARCHITECT.md. diff --git a/build_linux.sh b/build_linux.sh new file mode 100755 index 0000000..736e21f --- /dev/null +++ b/build_linux.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# build_linux.sh - build daw_engine (PyInstaller ONEDIR) + Tauri v2 (deb + AppImage) +# Chay tren Linux: bash build_linux.sh +# Yeu cau: python3, pip, node/npm, rust/cargo, webkit2gtk-4.1, libappindicator, +# librsvg (xem README / DISTRIBUTION_PLAN.md) +set -euo pipefail +cd "$(dirname "$0")" + +echo "== [1/6] Python dependencies ==" +# PyInstaller tren Linux can objdump (binutils). May build that phai co: +# sudo apt-get install -y binutils +python3 -m pip install --upgrade pip >/dev/null +python3 -m pip install -r requirements.txt pyinstaller + +echo "== [2/6] Frontend bundle (app.jsx -> app.precompiled.js) ==" +npm install --no-audit --no-fund +if [ ! -d "node_modules/@babel/standalone" ]; then + echo "Thieu @babel/standalone - dang cai them..." + npm install @babel/standalone --no-audit --no-fund +fi +node build.mjs + +echo "== [3/6] Build daw_engine (PyInstaller ONEDIR) ==" +python3 -m PyInstaller engine.spec --clean --noconfirm + +echo "== [3.5/6] Verify bundle contents (app/static, app/templates phai co) ==" +python3 tools/verify_bundle.py || { echo "ERROR: Bundle thieu asset - dung build!"; exit 1; } + +echo "== [4/6] Copy onedir engine -> src-tauri/resources/daw_engine ==" +if [ ! -f "dist/daw_engine/daw_engine" ]; then + echo "ERROR: dist/daw_engine/daw_engine khong ton tai (onedir build loi?)" + exit 1 +fi +rm -rf src-tauri/resources/daw_engine +mkdir -p src-tauri/resources/daw_engine +cp -a dist/daw_engine/. src-tauri/resources/daw_engine/ +echo "Copied onedir engine -> src-tauri/resources/daw_engine" + +echo "== [5/6] Kiem tra resources truoc khi tauri build ==" +if [ ! -f "src-tauri/resources/daw_engine/daw_engine" ] || [ ! -d "src-tauri/resources/daw_engine/_internal" ]; then + echo "ERROR: thieu src-tauri/resources/daw_engine/{daw_engine,_internal}" + exit 1 +fi + +echo "== [6/6] Tauri build (deb + AppImage) ==" +npm install -D @tauri-apps/cli --no-audit --no-fund +npx tauri build + +echo "" +echo "== DONE ==" +echo " deb : src-tauri/target/release/bundle/deb/sonicforge-daw_1.0.0_amd64.deb" +echo " AppImage: src-tauri/target/release/bundle/appimage/SonicForgeDAW_1.0.0_amd64.AppImage" diff --git a/build_macos.sh b/build_macos.sh new file mode 100755 index 0000000..92f669b --- /dev/null +++ b/build_macos.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# build_macos.sh - build daw_engine (PyInstaller ONEDIR) + Tauri v2 (.app + .dmg) +# Chay tren macOS: bash build_macos.sh +# LUU Y: macOS yeu cau codesign + notarize truoc khi phat hanh ra ngoai +# (Gatekeeper). Xem DISTRIBUTION_PLAN.md. +set -euo pipefail +cd "$(dirname "$0")" + +echo "== [1/6] Python dependencies ==" +python3 -m pip install --upgrade pip >/dev/null +python3 -m pip install -r requirements.txt pyinstaller + +echo "== [2/6] Frontend bundle ==" +npm install --no-audit --no-fund +if [ ! -d "node_modules/@babel/standalone" ]; then + npm install @babel/standalone --no-audit --no-fund +fi +node build.mjs + +echo "== [3/6] Build daw_engine (PyInstaller ONEDIR) ==" +python3 -m PyInstaller engine.spec --clean --noconfirm + +echo "== [3.5/6] Verify bundle contents ==" +python3 tools/verify_bundle.py || { echo "ERROR: Bundle thieu asset - dung build!"; exit 1; } + +echo "== [4/6] Copy onedir engine -> src-tauri/resources/daw_engine ==" +if [ ! -f "dist/daw_engine/daw_engine" ]; then + echo "ERROR: dist/daw_engine/daw_engine khong ton tai (onedir build loi?)" + exit 1 +fi +rm -rf src-tauri/resources/daw_engine +mkdir -p src-tauri/resources/daw_engine +cp -a dist/daw_engine/. src-tauri/resources/daw_engine/ +echo "Copied onedir engine -> src-tauri/resources/daw_engine" + +echo "== [5/6] Kiem tra resources ==" +if [ ! -f "src-tauri/resources/daw_engine/daw_engine" ] || [ ! -d "src-tauri/resources/daw_engine/_internal" ]; then + echo "ERROR: thieu src-tauri/resources/daw_engine/{daw_engine,_internal}" + exit 1 +fi + +echo "== [6/6] Tauri build (dmg) ==" +npm install -D @tauri-apps/cli --no-audit --no-fund +npx tauri build + +echo "" +echo "== DONE ==" +echo " dmg: src-tauri/target/release/bundle/dmg/SonicForgeDAW_1.0.0_x64.dmg" +echo " (Codesign/notarize: codesign --deep -s \"Developer ID Application: ...\" " +echo " src-tauri/target/release/bundle/macos/SonicForgeDAW.app ; xcrun notarytool submit ...)" diff --git a/engine.spec b/engine.spec index fe1f417..1215c88 100644 --- a/engine.spec +++ b/engine.spec @@ -1,11 +1,11 @@ -# engine.spec — PyInstaller config cho daw_engine.exe (sidecar Python) -# Chay: pyinstaller engine.spec --clean --noconfirm (tren Windows) +# engine.spec — PyInstaller config cho daw_engine (sidecar Python) +# Chay: pyinstaller engine.spec --clean --noconfirm (Windows/Linux/macOS) # -*- mode: python ; coding: utf-8 -*- import os -from PyInstaller.utils.hooks import collect_dynamic_libs, collect_submodules, collect_data_files +from PyInstaller.utils.hooks import collect_dynamic_libs, collect_data_files -# Native DLL cho pedalboard va soundfile +# Native DLL cho pedalboard va soundfile (Windows .pyd/.dll, Linux .so) binaries = collect_dynamic_libs('pedalboard') binaries += collect_dynamic_libs('soundfile') @@ -14,7 +14,7 @@ binaries += collect_dynamic_libs('soundfile') # duoc bundle qua collect_data_files('app'). _SPEC_ROOT = os.path.abspath(SPECPATH) -# ⚠️ GOC ROOT CUA MOI LOI 'app\static does not exist' (gap 3 lan): +# ⚠️ GOC ROOT CUA MOI LOI 'app\\static does not exist' (gap 3 lan): # lenh `pyinstaller engine.spec` (entry-point script) KHONG them CWD vao # sys.path (chi `python -m PyInstaller` moi them). collect_data_files('app') # import package qua sys.path -> khong thay 'app' -> tra ve [] AM THAM -> @@ -25,8 +25,8 @@ if _SPEC_ROOT not in _sys.path: _sys.path.insert(0, _SPEC_ROOT) # Assets cua app: bundle QUA IMPORT SYSTEM (collect_data_files) — an toan nhat. -# Loai tru storage (57MB soundfonts/uploads — vo ich trong onefile, config.py -# da chuyen storage sang %APPDATA%\\SonicForgeDAW khi frozen) va __pycache__. +# Loai tru storage (57MB soundfonts/uploads — vo ich, config.py da chuyen +# storage sang %APPDATA%\\SonicForgeDAW khi frozen) va __pycache__. datas = collect_data_files('app', excludes=['**/storage/**', '**/__pycache__/**', '**/*.pyc']) # Fallback cuoi cung: neu collect_data_files van tra ve rong (phong moi truong # hop ky la), dung datas TINH absolute — tinh huong xau nhat van co du assets. @@ -41,66 +41,87 @@ datas += [ (os.path.join(_SPEC_ROOT, 'md'), 'md'), # /ai-prompt-generator (doc, ngoai package app) ] -# librosa 0.11 dùng lazy_loader.attach_stub -> lúc RUNTIME cần file .pyi -# ton tai tren disk ('Cannot load imports from non-existent stub ...librosa\__init__.pyi'). -# PyInstaller mac dinh KHONG bundle .pyi -> phai collect explicit. -datas += collect_data_files('librosa', includes=['**/*.pyi']) - -# scipy >= 1.18 tach scipy.stats thanh nhieu module con (vd -# _ansari_swilk_statistics) import lazy ben trong ham -> hook scipy cua -# PyInstaller miss -> ModuleNotFoundError luc runtime. Giai phap TRIET DE: -# scan FILESYSTEM toan bo site-packages/scipy (khong import, khong walk — -# pkgutil.walk_packages BO QUA AM THAM subpackage import loi luc build, -# da gap: may user mat ca cay scipy.sparse.csgraph._shortest_path). -# Bat moi module .py + C-extension .pyd/.so -> hiddenimports day du. +# ══════════════════════════════════════════════════════════════════════════ +# TOI UU BUNDLE (SonicForgeStudio 1.1): 409MB -> ~120MB +# ────────────────────────────────────────────────────────────────────────── +# 1. librosa/numba/llvmlite (~171MB) + scikit-learn (~17MB) da DUOC LOAI BO +# khoi code (app/core/audio_features.py thay the, numpy/scipy/soundfile). +# 2. celery/kombu/billiard/redis (~40MB) KHONG bundle — desktop chay task +# eager dong bo, khong can broker (app/api/v1/tasks.py da lazy + fallback). +# 3. scipy: KHONG con quet toan bo site-packages/scipy (truoc day bundle ca +# scipy.stats/sparse/optimize/linalg ~48MB). Chi quet scipy.signal — goi +# lazy-import noi bo cua no van duoc bat day du (scipy.signal.windows, +# _savitzky_golay, _spectral_py... duoc import bang ten ben trong ham). +# scipy.signal la goi DUY NHAT con duoc app dung (sub_tab_dsp, +# render_engine, audio_features). +# ══════════════════════════════════════════════════════════════════════════ import importlib.util as _ilu import glob as _glob -_scipy_spec = _ilu.find_spec('scipy') -_scipy_dir = os.path.dirname(os.path.abspath(_scipy_spec.origin)) -_scipy_hidden = [] -for _ext in ('*.py', '*.pyd', '*.so'): - for _f in _glob.glob(os.path.join(_scipy_dir, '**', _ext), recursive=True): - _rel = os.path.relpath(_f, _scipy_dir) - _base = os.path.basename(_rel).split('.')[0] # bo .cpython-312-x86_64... .so - _pkg = os.path.dirname(_rel).replace(os.sep, '.') - _mod = ('scipy.' + _pkg + '.' + _base) if _pkg else ('scipy.' + _base) - if _mod not in _scipy_hidden: - _scipy_hidden.append(_mod) -# Cung co bang hiddenimport TINH: _morestats import module nay o top-level -# (scipy 1.18+); collect_submodules du phong nhung neu miss (version khac -# tren may user) thi dong nay van dam bao bundle co. -if 'scipy.stats._ansari_swilk_statistics' not in _scipy_hidden: - _scipy_hidden.append('scipy.stats._ansari_swilk_statistics') -# scipy.sparse.csgraph cung lazy-import C-extension tu ben trong ham (vd -# _shortest_path, _traversal, _matching) — hiddenimport tinh phong walk miss. -for _m in ('scipy.sparse.csgraph._shortest_path', 'scipy.sparse.csgraph._traversal', - 'scipy.sparse.csgraph._matching', 'scipy.sparse.csgraph._min_spanning_tree'): - if _m not in _scipy_hidden: - _scipy_hidden.append(_m) + +def _scan_pkg_modules(pkg_name: str): + """Scan filesystem cua 1 package con (khong import, khong walk) -> + bat moi module .py/.pyd/.so -> hiddenimports day du, tranh lazy-import miss.""" + _spec = _ilu.find_spec(pkg_name) + if _spec is None or _spec.origin is None: + print(f"WARN: khong tim thay package '{pkg_name}' - bo qua scan") + return [] + _pkg_dir = os.path.dirname(os.path.abspath(_spec.origin)) + _out = [] + for _ext in ('*.py', '*.pyd', '*.so'): + for _f in _glob.glob(os.path.join(_pkg_dir, '**', _ext), recursive=True): + _rel = os.path.relpath(_f, _pkg_dir) + _base = os.path.basename(_rel).split('.')[0] # bo .cpython-312-x86_64... .so + _sub = os.path.dirname(_rel).replace(os.sep, '.') + _mod = (pkg_name + '.' + _sub + '.' + _base) if _sub else (pkg_name + '.' + _base) + if _mod not in _out: + _out.append(_mod) + return _out + +_scipy_signal_hidden = _scan_pkg_modules('scipy.signal') + +# Uvicorn lazy-load loop/protocol theo ten (string) -> hiddenimport tinh. +_hidden = [ + 'uvicorn.logging', + 'uvicorn.loops', + 'uvicorn.loops.auto', + 'uvicorn.protocols', + 'uvicorn.protocols.http', + 'uvicorn.protocols.http.auto', + 'uvicorn.protocols.websockets', + 'uvicorn.protocols.websockets.auto', + 'pedalboard', + 'soundfile', + 'sf2utils', + 'mido.backends.rtmidi', +] + _scipy_signal_hidden + +# Khoa khong bundle: loai toan bo cay nang khong con duoc dung. +_excludes = [ + 'tkinter', + # libs da thay the (audio_features.py) + 'librosa', 'numba', 'llvmlite', 'sklearn', 'scikit-learn', + 'joblib', 'threadpoolctl', 'audioread', 'lazy_loader', 'soxr', + # celery/redis chi dung cho server (Docker), khong cho desktop + 'celery', 'kombu', 'billiard', 'vine', 'amqp', 'redis', + 'click_didyoumean', 'click_plugins', 'click_repl', + # LUU Y: KHONG exclude 'click' — uvicorn.main import click (CLI parser)! + 'dateutil', 'pytz', 'tzdata', 'msgpack', 'yaml', + # khong dung trong desktop + 'matplotlib', 'pandas', 'IPython', 'jupyter', 'pytest', 'setuptools', + # keo vao nham boi hooks_contrib (app khong import bao gio) + 'PIL', 'Pillow', 'cairosvg', 'zstandard', 'imageio', +] a = Analysis( ['desktop_engine.py'], pathex=[_SPEC_ROOT], binaries=binaries, datas=datas, - hiddenimports=collect_submodules('celery.fixups') + _scipy_hidden + [ - 'uvicorn.logging', - 'uvicorn.loops', - 'uvicorn.loops.auto', - 'uvicorn.protocols', - 'uvicorn.protocols.http', - 'uvicorn.protocols.http.auto', - 'uvicorn.protocols.websockets', - 'uvicorn.protocols.websockets.auto', - 'pedalboard', - 'soundfile', - 'sf2utils', - 'mido.backends.rtmidi', - ], + hiddenimports=_hidden, hookspath=[], hooksconfig={}, runtime_hooks=[], - excludes=['tkinter'], + excludes=_excludes, win_no_prefer_redirects=False, win_private_assemblies=False, cipher=None, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index a7c40d6..c6a0c19 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -25,7 +25,7 @@ }, "bundle": { "active": true, - "targets": ["msi"], + "targets": ["nsis", "msi"], "icon": [ "icons/32x32.png", "icons/128x128.png",