feat(rt+auth): batched input publish boi so 4 (het crackle) + auto-login loopback khong can mat khau
- fx_realtime: write_input gom FXRT_IN_SLOTS(4) frame vao _in_pending, flush ghi 1 lan + h.in_write += n (publish atomic -> bridge take=4 luon, het deficit) - plugins pump: flush_input_stale(0.004) dau vong lap -> input le khong ket - auth: _is_loopback(127.*/::1/localhost) -> get_current_user khong token = admin tu DB (must_change_password=False); POST /auth/local auto-login loopback, LAN 403 - ws fx-realtime: loopback khong token cho phep, remote thieu token -> close 4401 - UI: checkAuthStatus khong savedToken -> thu localLogin, thanh cong vao DAW, that bai -> modal login nhu cu; cache-buster v=202608212400
This commit is contained in:
@@ -52,6 +52,30 @@ def _record_login_success(ip: str):
|
||||
with _LOGIN_LOCK:
|
||||
_LOGIN_FAILURES.pop(ip, None)
|
||||
|
||||
def _is_loopback(host: Optional[str]) -> bool:
|
||||
"""Standalone: request từ chính máy (127.0.0.1/::1/localhost) → bỏ qua
|
||||
đăng nhập. Client LAN (IP khác) → vẫn cần mật khẩu."""
|
||||
if not host:
|
||||
return False
|
||||
host = host.lower()
|
||||
return host in ("127.0.0.1", "::1", "localhost") or host.startswith("127.")
|
||||
|
||||
def _loopback_admin_user() -> dict:
|
||||
"""Identity admin cho loopback — real user_id từ DB; không bắt buộc đổi
|
||||
mật khẩu (standalone tự vào thẳng DAW)."""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
cur = conn.execute("SELECT id, username, role FROM users WHERE username = ?", ("admin",))
|
||||
row = cur.fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
if row:
|
||||
return {"user_id": row["id"], "username": row["username"], "role": row["role"], "must_change_password": False}
|
||||
except Exception:
|
||||
pass
|
||||
return {"user_id": "local", "username": "admin", "role": "admin", "must_change_password": False}
|
||||
|
||||
def _set_auth_cookie(response: Response, token: str):
|
||||
response.set_cookie(
|
||||
COOKIE_NAME, token,
|
||||
@@ -68,6 +92,10 @@ def get_current_user(request: Request, authorization: Optional[str] = Header(Non
|
||||
elif request.cookies.get(COOKIE_NAME):
|
||||
token = request.cookies.get(COOKIE_NAME)
|
||||
if not token:
|
||||
# Standalone (loopback): không cần mật khẩu — auto admin. LAN vẫn 401.
|
||||
host = request.client.host if request.client else None
|
||||
if _is_loopback(host):
|
||||
return _loopback_admin_user()
|
||||
raise HTTPException(status_code=401, detail="Thiếu Token xác thực hoặc Token không hợp lệ")
|
||||
payload = decode_token(token)
|
||||
if not payload:
|
||||
@@ -135,6 +163,22 @@ async def login(req: LoginRequest, request: Request):
|
||||
_set_auth_cookie(resp, token)
|
||||
return resp
|
||||
|
||||
@router.post("/local")
|
||||
async def local_login(request: Request, response: Response):
|
||||
"""Auto-login standalone: chỉ cho phép từ loopback (máy chạy engine).
|
||||
Client LAN nhận 403 → buộc đăng nhập mật khẩu qua /login."""
|
||||
host = request.client.host if request.client else None
|
||||
if not _is_loopback(host):
|
||||
raise HTTPException(status_code=403, detail="Truy cập từ xa — cần đăng nhập")
|
||||
u = _loopback_admin_user()
|
||||
token = create_token(u["user_id"], u["username"], u["role"], False)
|
||||
resp = JSONResponse({
|
||||
"access_token": token,
|
||||
"user": {"id": u["user_id"], "username": u["username"], "role": u["role"], "must_change_password": False},
|
||||
})
|
||||
_set_auth_cookie(resp, token)
|
||||
return resp
|
||||
|
||||
def _validate_password_strength(password: str):
|
||||
"""Minimal strength policy: >= 8 chars and not trivially common."""
|
||||
if len(password) < 8:
|
||||
|
||||
@@ -11,7 +11,7 @@ from app.core.render_engine import PythonRenderEngine, _find_sf2_path, _find_def
|
||||
from app.core.soundfont_inspector import SoundFontInspector
|
||||
from app.core.soundfont_converter import SoundFontConverter
|
||||
from app.core.soundfont_scanner import SoundFontAutoScanner
|
||||
from app.api.v1.auth import get_current_user, enforce_password_changed
|
||||
from app.api.v1.auth import get_current_user, enforce_password_changed, _is_loopback
|
||||
from app.core import fx_realtime
|
||||
from app.core.auth import decode_token
|
||||
|
||||
@@ -1038,8 +1038,8 @@ async def ws_fx_realtime(websocket: WebSocket, session_id: str):
|
||||
"""Vòng lặp FX realtime: receive block input → SHM → bridge → SHM → send
|
||||
block output. Binary frames: 2048B stereo float32 interleaved (256*2)."""
|
||||
# Auth: token qua query param (JS WebSocket không set header dễ) hoặc
|
||||
# Authorization Bearer. Desktop app có thể chưa login → token rỗng được
|
||||
# phép (endpoint /start vẫn yêu cầu auth).
|
||||
# Authorization Bearer. Standalone (loopback) không cần token — auto admin
|
||||
# (đồng bộ get_current_user). Client LAN thiếu token → close.
|
||||
token = websocket.query_params.get("token", "") or ""
|
||||
if not token:
|
||||
auth = websocket.headers.get("authorization", "")
|
||||
@@ -1053,6 +1053,11 @@ async def ws_fx_realtime(websocket: WebSocket, session_id: str):
|
||||
if not payload:
|
||||
await websocket.close(code=4401)
|
||||
return
|
||||
else:
|
||||
_host = websocket.client.host if websocket.client else None
|
||||
if not _is_loopback(_host):
|
||||
await websocket.close(code=4401)
|
||||
return
|
||||
sess = fx_realtime.get_session(session_id)
|
||||
if not sess:
|
||||
await websocket.close(code=4404)
|
||||
@@ -1074,6 +1079,7 @@ async def ws_fx_realtime(websocket: WebSocket, session_id: str):
|
||||
_loop = asyncio.get_event_loop()
|
||||
while True:
|
||||
try:
|
||||
sess.flush_input_stale() # input lẻ kẹt trong pending → publish
|
||||
_lats = sess.get_latencies()
|
||||
_total = sum(_lats.values()) if _lats else 0
|
||||
if _total != _last_lat:
|
||||
|
||||
+46
-13
@@ -113,6 +113,11 @@ class FxRealtimeSession:
|
||||
self.h.lat_slots = FXRT_LAT_SLOTS
|
||||
self._np = np.frombuffer(self.shm.buf, dtype=np.float32)
|
||||
self._lock = threading.Lock()
|
||||
# Input batching (fix crackle): gom FXRT_IN_SLOTS frame rồi publish 1 lần
|
||||
# (in_write += n atomic) → bridge luôn thấy bội số của FXRT_IN_SLOTS →
|
||||
# take=4 → batch xử lí. Không phụ thuộc client gửi burst hay lẻ.
|
||||
self._in_pending = []
|
||||
self._in_pending_t0 = None
|
||||
self.proc = None
|
||||
self._job_path = ""
|
||||
self._started_at = time.time()
|
||||
@@ -213,24 +218,52 @@ class FxRealtimeSession:
|
||||
|
||||
# ── audio I/O ───────────────────────────────────────────────────────────
|
||||
def write_input(self, interleaved):
|
||||
"""Ghi 1 block stereo float32 (2048 bytes) vào input ring. Ring đầy →
|
||||
drop block cũ nhất (giữ latency thấp)."""
|
||||
"""Gom FXRT_IN_SLOTS frame vào pending, đủ 4 → publish atomic vào ring
|
||||
(ghi data toàn bộ slot trước, sau đó in_write += n 1 lần). Ring đầy khi
|
||||
publish → drop batch cũ nhất (giữ latency thấp)."""
|
||||
arr = np.frombuffer(interleaved, dtype=np.float32)
|
||||
if arr.size != self.block_size * 2:
|
||||
return
|
||||
arr = arr.reshape(self.block_size, 2)
|
||||
with self._lock:
|
||||
h = self.h
|
||||
while True:
|
||||
if h.in_write - h.in_read < h.in_slots:
|
||||
slot = h.in_write & (h.in_slots - 1)
|
||||
bl = IN_L_OFF // 4 + slot * self.block_size
|
||||
br = IN_R_OFF // 4 + slot * self.block_size
|
||||
self._np[bl:bl + self.block_size] = arr[:, 0]
|
||||
self._np[br:br + self.block_size] = arr[:, 1]
|
||||
h.in_write += 1
|
||||
return
|
||||
h.in_read += 1 # full → drop oldest input
|
||||
if not self._in_pending:
|
||||
self._in_pending_t0 = time.time()
|
||||
self._in_pending.append((arr[:, 0].copy(), arr[:, 1].copy()))
|
||||
if len(self._in_pending) >= FXRT_IN_SLOTS:
|
||||
self._flush_input_locked()
|
||||
|
||||
def _flush_input_locked(self):
|
||||
"""Publish pending vào input ring. Ghi data trước, in_write += n sau →
|
||||
bridge đọc in_write 1 lần → thấy nguyên batch (take=4)."""
|
||||
if not self._in_pending:
|
||||
return
|
||||
h = self.h
|
||||
frames = self._in_pending
|
||||
self._in_pending = []
|
||||
self._in_pending_t0 = None
|
||||
avail = h.in_slots - (h.in_write - h.in_read)
|
||||
if avail <= 0:
|
||||
h.in_read += len(frames) # ring full → drop cả batch
|
||||
return
|
||||
n = min(len(frames), avail)
|
||||
drop = len(frames) - n
|
||||
if drop > 0:
|
||||
h.in_read += drop # drop frame cũ nhất
|
||||
for i in range(n):
|
||||
L, R = frames[i + drop]
|
||||
slot = (h.in_write + i) & (h.in_slots - 1)
|
||||
bl = IN_L_OFF // 4 + slot * self.block_size
|
||||
br = IN_R_OFF // 4 + slot * self.block_size
|
||||
self._np[bl:bl + self.block_size] = L
|
||||
self._np[br:br + self.block_size] = R
|
||||
h.in_write += n # publish atomic — bridge thấy đủ n block cùng lúc
|
||||
|
||||
def flush_input_stale(self, timeout=0.004):
|
||||
"""Flush pending nếu frame đầu chờ > timeout (client gửi lẻ/ngừng giữa
|
||||
burst) → không kẹt latency. Gọi từ _pump_output mỗi vòng."""
|
||||
with self._lock:
|
||||
if self._in_pending and self._in_pending_t0 is not None and time.time() - self._in_pending_t0 > timeout:
|
||||
self._flush_input_locked()
|
||||
|
||||
def read_output(self):
|
||||
"""Drain output ring → list bytes (mỗi block 2048 bytes interleaved)."""
|
||||
|
||||
+19
-2
@@ -1711,7 +1711,7 @@ async function fxRtStart() {
|
||||
fxRt.sessionId = st.session_id;
|
||||
fxRtGuiPushShm();
|
||||
try {
|
||||
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608212300`);
|
||||
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608212400`);
|
||||
fxRt.node = new AudioWorkletNode(ctx, 'sf-fx-realtime', { numberOfInputs: 1, numberOfOutputs: 1, outputChannelCount: [2] });
|
||||
const u = new URL(API_BASE_URL);
|
||||
const wsProto = u.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
@@ -19382,6 +19382,23 @@ const App = () => {
|
||||
const checkAuthStatus = async () => {
|
||||
const savedToken = localStorage.getItem('sonic_token');
|
||||
if (!savedToken) {
|
||||
// Standalone (loopback): auto-login không cần mật khẩu. LAN client:
|
||||
// /auth/local trả 403 → hiện modal đăng nhập như cũ.
|
||||
try {
|
||||
const res = await window.SonicAPI.localLogin();
|
||||
if (res && res.access_token) {
|
||||
localStorage.setItem('sonic_token', res.access_token);
|
||||
localStorage.setItem('sonic_user', JSON.stringify(res.user));
|
||||
setCurrentUser(res.user);
|
||||
setIsMandatoryLogin(false);
|
||||
setAuthModalOpen(false);
|
||||
(async () => {
|
||||
const restored = await restoreLastSessionProject();
|
||||
if (!restored) await restoreTempProjectIfFresh();
|
||||
})();
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
setIsMandatoryLogin(true);
|
||||
setAuthMode('login');
|
||||
setAuthModalOpen(true);
|
||||
@@ -23662,7 +23679,7 @@ const App = () => {
|
||||
fxRtTracks[key] = entry;
|
||||
fxRtGuiPushShm();
|
||||
try {
|
||||
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608212300`);
|
||||
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608212400`);
|
||||
const node = new AudioWorkletNode(ctx, 'sf-fx-realtime', { numberOfInputs: 1, numberOfOutputs: 1, outputChannelCount: [2] });
|
||||
entry.node = node;
|
||||
const u = new URL(API_BASE_URL);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -35,6 +35,7 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
|
||||
|
||||
window.SonicAPI = {
|
||||
login: (username, password) => apiRequest('/api/v1/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) }),
|
||||
localLogin: () => apiRequest('/api/v1/auth/local', { method: 'POST' }),
|
||||
register: (username, email, password) => apiRequest('/api/v1/auth/register', { method: 'POST', body: JSON.stringify({ username, email, password }) }),
|
||||
changePassword: (old_password, new_password) => apiRequest('/api/v1/auth/change-password', { method: 'POST', body: JSON.stringify({ old_password, new_password }) }),
|
||||
getProfile: () => apiRequest('/api/v1/auth/profile', { method: 'GET' }),
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<script src="/static/vendor/react-dom.production.min.js"></script>
|
||||
<script src="/static/js/services/fluidsynthLoader.js?v=202607271245"></script>
|
||||
<script src="/static/js/services/runtime.js?v=202608101800"></script>
|
||||
<script src="/static/js/services/api.js?v=202608152300"></script>
|
||||
<script src="/static/js/services/api.js?v=202608212400"></script>
|
||||
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/storage.js?v=202608038200"></script>
|
||||
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
|
||||
@@ -51,7 +51,7 @@
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/services/chordTheory.js?v=202608211300"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608212300" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608212400" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user