diff --git a/src/chat.rs b/src/chat.rs index 47c18b3..10beca6 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -23,6 +23,10 @@ pub enum ServerEvent { other_user_id: Uuid, message: serde_json::Value, }, + UserPresence { + user_id: Uuid, + online: bool, + }, } #[derive(Deserialize)] @@ -43,6 +47,18 @@ impl ChatHub { clients.remove(&user_id); } + pub async fn get_online_users(&self) -> Vec { + let clients = self.clients.read().await; + clients.keys().cloned().collect() + } + + pub async fn broadcast_all(&self, event: ServerEvent) { + let clients = self.clients.read().await; + for tx in clients.values() { + let _ = tx.send(event.clone()); + } + } + pub async fn broadcast_to_user(&self, user_id: Uuid, event: ServerEvent) { let clients = self.clients.read().await; if let Some(tx) = clients.get(&user_id) { @@ -65,6 +81,13 @@ pub async fn handle_socket(state: AppState, socket: WebSocket, user_id: Uuid) { let (tx, mut rx) = mpsc::unbounded_channel::(); state.chat.add_client(user_id, tx).await; + state + .chat + .broadcast_all(ServerEvent::UserPresence { + user_id, + online: true, + }) + .await; let send_task = tokio::spawn(async move { while let Some(event) = rx.recv().await { @@ -85,4 +108,11 @@ pub async fn handle_socket(state: AppState, socket: WebSocket, user_id: Uuid) { send_task.abort(); state.chat.remove_client(user_id).await; + state + .chat + .broadcast_all(ServerEvent::UserPresence { + user_id, + online: false, + }) + .await; } diff --git a/src/handlers.rs b/src/handlers.rs index fa9dc93..ffa363b 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -26,6 +26,7 @@ pub fn routes() -> Router { "/dms/{other_user_id}/messages", get(list_dm_messages).post(send_dm_message), ) + .route("/presence", get(presence_list)) .route("/rtc-config", get(rtc_config)) .route("/guilds", get(list_guilds).post(create_guild)) .route("/guilds/{guild_id}/members", get(list_guild_members)) @@ -293,6 +294,14 @@ async fn me(State(state): State, user: AuthUser) -> Result, + _user: AuthUser, +) -> Result { + let users = state.chat.get_online_users().await; + Ok(Json(users)) +} + async fn list_dm_conversations( State(state): State, user: AuthUser, diff --git a/static/app.js b/static/app.js index 62ecc6a..8f4347f 100644 --- a/static/app.js +++ b/static/app.js @@ -23,6 +23,7 @@ const state = { voicePresencePollId: null, chatWs: null, lastMessageId: null, + onlineUsers: new Set(), }; const el = { @@ -141,6 +142,14 @@ function formatDate(isoString) { } let audioCtx = null; + +// Global interaction listener to unlock AudioContext (autoplay policy) +window.addEventListener('click', () => { + if (audioCtx && audioCtx.state === 'suspended') { + audioCtx.resume(); + } +}, { once: true }); + function playSound(type) { try { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); @@ -274,9 +283,16 @@ function renderChannels() { function renderDMs() { el.dmList.innerHTML = ""; for (const dm of state.dmConversations) { + const isOnline = state.onlineUsers.has(dm.user_id); const row = document.createElement("button"); row.className = `channel-row ${state.selectedDmUserId === dm.user_id ? "active" : ""}`; - row.innerHTML = ` ${escapeHtml(dm.display_name)}`; + row.innerHTML = ` +
+ +
+
+ ${escapeHtml(dm.display_name)} + `; row.onclick = async () => { state.selectedDmUserId = dm.user_id; state.selectedDmDisplayName = dm.display_name; @@ -329,11 +345,7 @@ function renderMessages(messages) { } if (messages.length > 0) { - const latest = messages[0]; - if (state.lastMessageId && latest.id !== state.lastMessageId && latest.author_user_id !== state.me?.id) { - playSound(state.selectedDmUserId ? 'dm' : 'message'); - } - state.lastMessageId = latest.id; + state.lastMessageId = messages[0].id; } el.messageList.scrollTop = el.messageList.scrollHeight; @@ -342,10 +354,14 @@ function renderMessages(messages) { function renderMembers() { el.memberList.innerHTML = ""; for (const m of state.members) { + const isOnline = state.onlineUsers.has(m.id); const row = document.createElement("div"); row.className = "member-row"; row.innerHTML = ` -
${shortName(m.display_name)}
+
+ ${shortName(m.display_name)} +
+
${escapeHtml(m.display_name)}
`; row.style.cursor = "pointer"; @@ -476,21 +492,31 @@ function initChatWs() { ws.onmessage = (event) => { const msg = JSON.parse(event.data); + const authorId = msg.message?.author_user_id; + const isFromMe = authorId && state.me && authorId === state.me.id; + if (msg.type === "message_created") { + if (!isFromMe) playSound('message'); if (state.selectedTextChannelId === msg.channel_id) { - // Optimization: we could just push the message, but refresh for now api(`/channels/${state.selectedTextChannelId}/messages?limit=100`).then(renderMessages); - } else { - // Even if not active, play sound - playSound('message'); } } else if (msg.type === "dm_created") { + if (!isFromMe) playSound('dm'); if (state.selectedDmUserId === msg.other_user_id) { api(`/dms/${state.selectedDmUserId}/messages?limit=100`).then(renderMessages); - } else { playSound('dm'); loadDMConversations().then(renderDMs); + } else { + loadDMConversations().then(renderDMs); } + } else if (msg.type === "user_presence") { + if (msg.online) { + state.onlineUsers.add(msg.user_id); + } else { + state.onlineUsers.delete(msg.user_id); + } + renderMembers(); + renderDMs(); } }; @@ -918,6 +944,11 @@ async function init() { await loadGuilds(); await loadDMConversations(); + try { + const online = await api("/presence"); + state.onlineUsers = new Set(online); + } catch (err) { console.warn("presence sync failed", err); } + if (state.guilds.length > 0) { const lastGuildId = localStorage.getItem(LAST_GUILD_STORAGE_KEY); const guild = state.guilds.find((g) => g.id === lastGuildId) || state.guilds[0]; diff --git a/static/styles.css b/static/styles.css index 012ceea..c1b9d6f 100644 --- a/static/styles.css +++ b/static/styles.css @@ -7,22 +7,24 @@ --bg-modifier-selected: rgba(78, 80, 88, 0.6); --bg-modifier-hover: rgba(78, 80, 88, 0.3); --bg-input: #383a40; - + --text-normal: #dbdee1; --text-muted: #949ba4; --text-strong: #f2f3f5; --text-link: #00a8fc; - + --brand: #5865f2; --brand-hover: #4752c4; --green: #23a559; --danger: #f23f43; --yellow: #f0b232; - + --font-main: "gg sans", "Inter", "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; } -* { box-sizing: border-box; } +* { + box-sizing: border-box; +} body { margin: 0; @@ -34,15 +36,46 @@ body { } /* Scrollbars */ -::-webkit-scrollbar { width: 8px; height: 8px; } -::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { background: var(--bg-tertiary); border-radius: 4px; } -::-webkit-scrollbar-thumb:hover { background: #242529; } +::-webkit-scrollbar { + width: 8px; + height: 8px; +} -button { border: 0; cursor: pointer; transition: all 150ms ease; background: none; color: inherit; font: inherit; padding: 0; } -input, select { border: 0; outline: none; background: var(--bg-input); color: var(--text-normal); font: inherit; } +::-webkit-scrollbar-track { + background: transparent; +} -.hidden { display: none !important; } +::-webkit-scrollbar-thumb { + background: var(--bg-tertiary); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #242529; +} + +button { + border: 0; + cursor: pointer; + transition: all 150ms ease; + background: none; + color: inherit; + font: inherit; + padding: 0; +} + +input, +select { + border: 0; + outline: none; + background: var(--bg-input); + color: var(--text-normal); + font: inherit; +} + +.hidden { + display: none !important; +} /* Auth Screen */ .auth-screen { @@ -52,14 +85,16 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va background-image: url("https://discord.com/assets/f9a15998e94589d343f7.png"); background-size: cover; } + .auth-card { width: min(480px, 92vw); background: var(--bg-sidebar); border-radius: 8px; padding: 32px; - box-shadow: 0 2px 10px rgba(0,0,0,0.2); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); text-align: center; } + .brand-large { width: 80px; height: 80px; @@ -72,8 +107,17 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va font-weight: 800; margin: 0 auto 20px; } -.auth-card h1 { margin: 0 0 8px; color: var(--text-strong); } -.auth-card p { margin: 0 0 24px; color: var(--text-muted); } + +.auth-card h1 { + margin: 0 0 8px; + color: var(--text-strong); +} + +.auth-card p { + margin: 0 0 24px; + color: var(--text-muted); +} + #login-btn { width: 100%; background: var(--brand); @@ -83,7 +127,10 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va font-weight: 600; font-size: 16px; } -#login-btn:hover { background: var(--brand-hover); } + +#login-btn:hover { + background: var(--brand-hover); +} /* Main Layout */ .shell { @@ -104,7 +151,11 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va overflow-y: auto; scrollbar-width: none; } -.server-rail::-webkit-scrollbar { display: none; } + +.server-rail::-webkit-scrollbar { + display: none; +} + .guild-list { display: flex; flex-direction: column; @@ -112,7 +163,8 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va padding: 6px 0; } -.brand, .guild-pill { +.brand, +.guild-pill { width: 48px; height: 48px; border-radius: 50%; @@ -124,8 +176,16 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va background: var(--bg-sidebar); } -.brand { background: var(--brand); color: #fff; border-radius: 16px; margin-bottom: 2px; } -.brand:hover { border-radius: 16px; } +.brand { + background: var(--brand); + color: #fff; + border-radius: 16px; + margin-bottom: 2px; +} + +.brand:hover { + border-radius: 16px; +} .separator { width: 32px; @@ -135,7 +195,8 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va border-radius: 1px; } -.guild-pill:hover, .guild-pill.active { +.guild-pill:hover, +.guild-pill.active { border-radius: 16px; background: var(--brand); color: #fff; @@ -151,11 +212,23 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va border-radius: 0 4px 4px 0; transition: all 0.2s ease; } -.guild-pill:hover::before { height: 20px; } -.guild-pill.active::before { height: 40px; } -.action-pill { color: var(--green); } -.action-pill:hover { background: var(--green); color: #fff; } +.guild-pill:hover::before { + height: 20px; +} + +.guild-pill.active::before { + height: 40px; +} + +.action-pill { + color: var(--green); +} + +.action-pill:hover { + background: var(--green); + color: #fff; +} /* Channel Sidebar */ .channel-sidebar { @@ -171,11 +244,15 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va align-items: center; justify-content: space-between; border-bottom: 1px solid var(--bg-darker); - box-shadow: 0 1px 0 rgba(0,0,0,0.1); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); cursor: pointer; transition: background-color 0.1s; } -.sidebar-header:hover { background: var(--bg-modifier-hover); } + +.sidebar-header:hover { + background: var(--bg-modifier-hover); +} + .sidebar-header h2 { margin: 0; font-size: 15px; @@ -185,11 +262,13 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va overflow: hidden; text-overflow: ellipsis; } + .sidebar-header-actions { display: flex; align-items: center; gap: 8px; } + .header-action-btn { width: 24px; height: 24px; @@ -198,18 +277,27 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va place-items: center; color: var(--text-muted); } + .header-action-btn:hover { background: var(--bg-modifier-hover); color: var(--text-normal); } + .header-action-btn i { width: 16px; height: 16px; } -.sidebar-scroll { flex: 1; overflow-y: auto; padding-top: 12px; } +.sidebar-scroll { + flex: 1; + overflow-y: auto; + padding-top: 12px; +} + +.sidebar-group { + margin-bottom: 20px; +} -.sidebar-group { margin-bottom: 20px; } .group-title { padding: 0 8px 0 2px; display: flex; @@ -221,9 +309,21 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va letter-spacing: 0.24px; cursor: pointer; } -.group-title:hover { color: var(--text-normal); } -.group-toggle { width: 12px; height: 12px; margin-right: 2px; } -.group-title span { flex: 1; } + +.group-title:hover { + color: var(--text-normal); +} + +.group-toggle { + width: 12px; + height: 12px; + margin-right: 2px; +} + +.group-title span { + flex: 1; +} + .add-btn { width: 20px; height: 20px; @@ -233,13 +333,24 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va display: grid; place-items: center; } + .add-btn:hover { opacity: 1; background: var(--bg-modifier-hover); } -.add-btn i { width: 16px; height: 16px; } -.channel-list { padding: 0 8px; display: flex; flex-direction: column; gap: 2px; } +.add-btn i { + width: 16px; + height: 16px; +} + +.channel-list { + padding: 0 8px; + display: flex; + flex-direction: column; + gap: 2px; +} + .channel-row { display: flex; align-items: center; @@ -249,12 +360,28 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va font-weight: 500; gap: 6px; } -.channel-row:hover { background: var(--bg-modifier-hover); color: var(--text-normal); } -.channel-row.active { background: var(--bg-modifier-selected); color: var(--text-strong); } -.channel-row i { width: 20px; height: 20px; opacity: 0.6; } + +.channel-row:hover { + background: var(--bg-modifier-hover); + color: var(--text-normal); +} + +.channel-row.active { + background: var(--bg-modifier-selected); + color: var(--text-strong); +} + +.channel-row i { + width: 20px; + height: 20px; + opacity: 0.6; +} /* Sidebar Footer */ -.sidebar-footer { background: var(--bg-secondary); padding: 0; } +.sidebar-footer { + background: var(--bg-secondary); + padding: 0; +} .user-panel { padding: 8px; @@ -263,9 +390,15 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va gap: 8px; height: 52px; } -.user-panel:hover { background: var(--bg-modifier-hover); } -.avatar-wrapper { position: relative; } +.user-panel:hover { + background: var(--bg-modifier-hover); +} + +.avatar-wrapper { + position: relative; +} + .avatar { width: 32px; height: 32px; @@ -277,6 +410,7 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va font-weight: 700; font-size: 14px; } + .status-dot { position: absolute; bottom: -2px; @@ -286,9 +420,16 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va border-radius: 50%; border: 3px solid var(--bg-secondary); } -.status-dot.online { background: var(--green); } -.user-info { flex: 1; min-width: 0; } +.status-dot.online { + background: var(--green); +} + +.user-info { + flex: 1; + min-width: 0; +} + .display-name { font-size: 14px; font-weight: 600; @@ -297,9 +438,17 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va overflow: hidden; text-overflow: ellipsis; } -.user-status { font-size: 12px; color: var(--text-muted); } -.user-actions { display: flex; gap: 2px; } +.user-status { + font-size: 12px; + color: var(--text-muted); +} + +.user-actions { + display: flex; + gap: 2px; +} + .user-actions button { width: 32px; height: 32px; @@ -308,21 +457,57 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va place-items: center; color: var(--text-muted); } -.user-actions button:hover { background: var(--bg-modifier-hover); color: var(--text-normal); } -.user-actions i { width: 20px; height: 20px; } + +.user-actions button:hover { + background: var(--bg-modifier-hover); + color: var(--text-normal); +} + +.user-actions i { + width: 20px; + height: 20px; +} /* Voice Connection */ .voice-connection { padding: 8px; background: var(--bg-secondary); - border-bottom: 1px solid rgba(255,255,255,0.05); + border-bottom: 1px solid rgba(255, 255, 255, 0.05); } -.vc-info { display: flex; align-items: center; gap: 8px; } -.vc-icon { color: var(--green); width: 20px; } -.vc-text { flex: 1; display: flex; flex-direction: column; } -.vc-status { color: var(--green); font-size: 14px; font-weight: 700; } -.vc-name { color: var(--text-muted); font-size: 12px; } -.vc-actions { display: flex; gap: 4px; } + +.vc-info { + display: flex; + align-items: center; + gap: 8px; +} + +.vc-icon { + color: var(--green); + width: 20px; +} + +.vc-text { + flex: 1; + display: flex; + flex-direction: column; +} + +.vc-status { + color: var(--green); + font-size: 14px; + font-weight: 700; +} + +.vc-name { + color: var(--text-muted); + font-size: 12px; +} + +.vc-actions { + display: flex; + gap: 4px; +} + .vc-actions button { width: 32px; height: 32px; @@ -331,7 +516,11 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va display: grid; place-items: center; } -.vc-actions button:hover { background: var(--bg-modifier-hover); color: var(--text-normal); } + +.vc-actions button:hover { + background: var(--bg-modifier-hover); + color: var(--text-normal); +} /* Chat Pane */ .chat-pane { @@ -347,11 +536,21 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va display: flex; align-items: center; gap: 8px; - border-bottom: 1px solid rgba(0,0,0,0.2); - box-shadow: 0 1px 0 rgba(0,0,0,0.1); + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); +} + +.header-icon { + color: var(--text-muted); + width: 24px; +} + +.chat-header h3 { + margin: 0; + font-size: 16px; + font-weight: 700; + color: var(--text-strong); } -.header-icon { color: var(--text-muted); width: 24px; } -.chat-header h3 { margin: 0; font-size: 16px; font-weight: 700; color: var(--text-strong); } .message-list { flex: 1; @@ -365,7 +564,10 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va gap: 16px; margin-top: 1.0625rem; } -.msg:hover { background: rgba(0,0,0,0.05); } + +.msg:hover { + background: rgba(0, 0, 0, 0.05); +} .msg-avatar { width: 40px; @@ -379,36 +581,77 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va font-weight: 600; } -.msg-content { flex: 1; min-width: 0; } -.msg-header { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; } +.msg-content { + flex: 1; + min-width: 0; +} + +.msg-header { + display: flex; + align-items: baseline; + gap: 8px; + margin-bottom: 4px; +} + .msg-author { font-weight: 600; color: var(--text-strong); cursor: pointer; font-size: 1rem; } -.msg-author:hover { text-decoration: underline; } -.msg-time { font-size: 12px; color: var(--text-muted); } -.msg-body { color: var(--text-normal); line-height: 1.375rem; white-space: pre-wrap; word-wrap: break-word; } -.msg-grouped { margin-top: 0; padding-top: 0; padding-bottom: 0; } -.msg-grouped .msg-avatar, .msg-grouped .msg-header { display: none; } -.msg-grouped .msg-content { padding-left: 56px; } +.msg-author:hover { + text-decoration: underline; +} + +.msg-time { + font-size: 12px; + color: var(--text-muted); +} + +.msg-body { + color: var(--text-normal); + line-height: 1.375rem; + white-space: pre-wrap; + word-wrap: break-word; +} + +.msg-grouped { + margin-top: 0; + padding-top: 0; + padding-bottom: 0; +} + +.msg-grouped .msg-avatar, +.msg-grouped .msg-header { + display: none; +} + +.msg-grouped .msg-content { + padding-left: 56px; +} /* Chat Input */ -.chat-input-wrapper { padding: 0 16px 24px; } +.chat-input-wrapper { + padding: 0 16px 24px; +} + .message-form { background: var(--bg-input); border-radius: 8px; padding: 11px 16px; } + .message-form input { width: 100%; background: transparent; color: var(--text-normal); font-size: 16px; } -.message-form input::placeholder { color: var(--text-muted); } + +.message-form input::placeholder { + color: var(--text-muted); +} /* Member List */ .utility-sidebar { @@ -416,7 +659,13 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va display: flex; flex-direction: column; } -.member-list-wrapper { flex: 1; overflow-y: auto; padding: 12px 8px; } + +.member-list-wrapper { + flex: 1; + overflow-y: auto; + padding: 12px 8px; +} + .member-row { display: flex; align-items: center; @@ -426,7 +675,12 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va color: var(--text-muted); cursor: pointer; } -.member-row:hover { background: var(--bg-modifier-hover); color: var(--text-normal); } + +.member-row:hover { + background: var(--bg-modifier-hover); + color: var(--text-normal); +} + .member-avatar { width: 32px; height: 32px; @@ -439,23 +693,67 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va font-weight: 600; flex-shrink: 0; } + .member-name { - font-weight: 500; - font-size: 15px; - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +/* Presence Badges */ +.avatar-wrapper { + position: relative; + display: inline-block; +} + +.status-badge { + position: absolute; + bottom: -2px; + right: -2px; + width: 12px; + height: 12px; + border-radius: 50%; + border: 2px solid var(--bg-sidebar); + background: #747f8d; + /* Gray for offline */ +} + +.status-badge.online { + background: var(--green); +} + +.member-avatar, +.msg-avatar { + position: relative; +} + +.status-dot { + position: absolute; + bottom: 0; + right: 0; + width: 10px; + height: 10px; + border-radius: 50%; + border: 2px solid var(--bg-sidebar); + background: #747f8d; +} + +.status-dot.online { + background: var(--green); +} + /* Modals */ .modal-container { position: fixed; - top: 0; left: 0; right: 0; bottom: 0; - background: rgba(0,0,0,0.85); + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.85); display: grid; place-items: center; z-index: 1000; } + .modal { background: var(--bg-sidebar); width: min(440px, 95vw); @@ -463,8 +761,17 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va padding: 24px; color: var(--text-normal); } -.modal h2 { margin: 0 0 16px; text-align: center; color: var(--text-strong); } -.form-item { margin-bottom: 20px; } + +.modal h2 { + margin: 0 0 16px; + text-align: center; + color: var(--text-strong); +} + +.form-item { + margin-bottom: 20px; +} + .form-item label { display: block; font-size: 12px; @@ -472,6 +779,7 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va color: var(--text-muted); margin-bottom: 8px; } + .form-item input { width: 100%; padding: 10px; @@ -485,8 +793,17 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va justify-content: flex-end; gap: 16px; } -.cancel-btn { padding: 10px 20px; color: var(--text-strong); font-weight: 500; } -.cancel-btn:hover { text-decoration: underline; } + +.cancel-btn { + padding: 10px 20px; + color: var(--text-strong); + font-weight: 500; +} + +.cancel-btn:hover { + text-decoration: underline; +} + .submit-btn { background: var(--brand); color: #fff; @@ -494,12 +811,28 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va border-radius: 3px; font-weight: 600; } -.submit-btn:hover { background: var(--brand-hover); } + +.submit-btn:hover { + background: var(--brand-hover); +} /* Radio Group for Channel Type */ -.radio-group { display: flex; flex-direction: column; gap: 8px; } -.radio-item { cursor: pointer; position: relative; } -.radio-item input { position: absolute; opacity: 0; } +.radio-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.radio-item { + cursor: pointer; + position: relative; +} + +.radio-item input { + position: absolute; + opacity: 0; +} + .radio-box { display: flex; align-items: center; @@ -509,21 +842,45 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va border-radius: 4px; transition: all 0.1s; } -.radio-item input:checked + .radio-box { background: var(--bg-modifier-selected); color: var(--text-strong); } -.radio-box i { width: 24px; height: 24px; color: var(--text-muted); } -.radio-text { display: flex; flex-direction: column; } -.radio-text strong { font-size: 16px; } -.radio-text span { font-size: 12px; color: var(--text-muted); } + +.radio-item input:checked+.radio-box { + background: var(--bg-modifier-selected); + color: var(--text-strong); +} + +.radio-box i { + width: 24px; + height: 24px; + color: var(--text-muted); +} + +.radio-text { + display: flex; + flex-direction: column; +} + +.radio-text strong { + font-size: 16px; +} + +.radio-text span { + font-size: 12px; + color: var(--text-muted); +} /* Mobile Overlay */ .overlay { position: fixed; - top: 0; left: 0; right: 0; bottom: 0; + top: 0; + left: 0; + right: 0; + bottom: 0; background: rgba(0, 0, 0, 0.7); z-index: 90; opacity: 1; transition: opacity 0.2s ease; } + .overlay.hidden { opacity: 0; pointer-events: none; @@ -541,39 +898,50 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va background: transparent; margin-right: 8px; } + .mobile-toggle-btn:hover { background: var(--bg-modifier-hover); border-radius: 4px; } @media (max-width: 1100px) { - .shell { grid-template-columns: 72px 240px 1fr; } + .shell { + grid-template-columns: 72px 240px 1fr; + } + .utility-sidebar { position: fixed; - top: 0; bottom: 0; right: 0; + top: 0; + bottom: 0; + right: 0; width: 240px; z-index: 100; - box-shadow: -2px 0 10px rgba(0,0,0,0.5); + box-shadow: -2px 0 10px rgba(0, 0, 0, 0.5); transform: translateX(100%); transition: transform 0.2s ease; } + .utility-sidebar.active { transform: translateX(0); display: flex; } - - .chat-header .mobile-toggle-btn { display: flex; } + + .chat-header .mobile-toggle-btn { + display: flex; + } } @media (max-width: 768px) { .shell { grid-template-columns: 1fr; } - + /* Hide sidebars by default on mobile */ - .server-rail, .channel-sidebar { + .server-rail, + .channel-sidebar { position: fixed; - top: 0; bottom: 0; + top: 0; + bottom: 0; z-index: 100; transition: transform 0.2s ease; } @@ -583,25 +951,28 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va width: 72px; transform: translateX(-100%); } - + .channel-sidebar { left: 72px; width: 240px; - transform: translateX(-312px); /* 72 + 240 */ - box-shadow: 2px 0 10px rgba(0,0,0,0.5); + transform: translateX(-312px); + /* 72 + 240 */ + box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5); } /* When menu is active */ .shell.menu-open .server-rail { transform: translateX(0); } + .shell.menu-open .channel-sidebar { transform: translateX(0); - display: flex; /* Override display: none from previous rule */ + display: flex; + /* Override display: none from previous rule */ } /* Adjust chat header for mobile */ .chat-header { padding: 0 8px; } -} +} \ No newline at end of file diff --git a/static/vendor/deepfilternet3/index.esm.js b/static/vendor/deepfilternet3/index.esm.js deleted file mode 100644 index 7ad3ee9..0000000 --- a/static/vendor/deepfilternet3/index.esm.js +++ /dev/null @@ -1,249 +0,0 @@ -class AssetLoader { - constructor(config = {}) { - this.cdnUrl = config.cdnUrl ?? 'https://cdn.mezon.ai/AI/models/datas/noise_suppression/deepfilternet3'; - } - getCdnUrl(relativePath) { - return `${this.cdnUrl}/${relativePath}`; - } - getAssetUrls() { - return { - wasm: this.getCdnUrl('v2/pkg/df_bg.wasm'), - model: this.getCdnUrl('v2/models/DeepFilterNet3_onnx.tar.gz') - }; - } - async fetchAsset(url) { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to fetch asset: ${response.statusText}`); - } - return response.arrayBuffer(); - } -} -let defaultLoader = null; -function getAssetLoader(config) { - if (!defaultLoader || config) { - defaultLoader = new AssetLoader(config); - } - return defaultLoader; -} - -/** - * Creates a worklet module URL from inline code string - * This approach works with all bundlers without special configuration - */ -async function createWorkletModule(audioContext, workletCode) { - const blob = new Blob([workletCode], { type: 'application/javascript' }); - const blobUrl = URL.createObjectURL(blob); - await audioContext.audioWorklet.addModule(blobUrl); -} - -const WorkletMessageTypes = { - SET_SUPPRESSION_LEVEL: 'SET_SUPPRESSION_LEVEL', - SET_BYPASS: 'SET_BYPASS' -}; - -var workletCode = "(function () {\n 'use strict';\n\n let wasm;\n\n const heap = new Array(128).fill(undefined);\n\n heap.push(undefined, null, true, false);\n\n function getObject(idx) { return heap[idx]; }\n\n let heap_next = heap.length;\n\n function dropObject(idx) {\n if (idx < 132) return;\n heap[idx] = heap_next;\n heap_next = idx;\n }\n\n function takeObject(idx) {\n const ret = getObject(idx);\n dropObject(idx);\n return ret;\n }\n\n const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );\n\n if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }\n let cachedUint8Memory0 = null;\n\n function getUint8Memory0() {\n if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) {\n cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);\n }\n return cachedUint8Memory0;\n }\n\n function getStringFromWasm0(ptr, len) {\n ptr = ptr >>> 0;\n return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));\n }\n\n function addHeapObject(obj) {\n if (heap_next === heap.length) heap.push(heap.length + 1);\n const idx = heap_next;\n heap_next = heap[idx];\n\n heap[idx] = obj;\n return idx;\n }\n /**\n * Set DeepFilterNet attenuation limit.\n *\n * Args:\n * - lim_db: New attenuation limit in dB.\n * @param {number} st\n * @param {number} lim_db\n */\n function df_set_atten_lim(st, lim_db) {\n wasm.df_set_atten_lim(st, lim_db);\n }\n\n /**\n * Get DeepFilterNet frame size in samples.\n * @param {number} st\n * @returns {number}\n */\n function df_get_frame_length(st) {\n const ret = wasm.df_get_frame_length(st);\n return ret >>> 0;\n }\n\n let WASM_VECTOR_LEN = 0;\n\n function passArray8ToWasm0(arg, malloc) {\n const ptr = malloc(arg.length * 1, 1) >>> 0;\n getUint8Memory0().set(arg, ptr / 1);\n WASM_VECTOR_LEN = arg.length;\n return ptr;\n }\n /**\n * Create a DeepFilterNet Model\n *\n * Args:\n * - path: File path to a DeepFilterNet tar.gz onnx model\n * - atten_lim: Attenuation limit in dB.\n *\n * Returns:\n * - DF state doing the full processing: stft, DNN noise reduction, istft.\n * @param {Uint8Array} model_bytes\n * @param {number} atten_lim\n * @returns {number}\n */\n function df_create(model_bytes, atten_lim) {\n const ptr0 = passArray8ToWasm0(model_bytes, wasm.__wbindgen_malloc);\n const len0 = WASM_VECTOR_LEN;\n const ret = wasm.df_create(ptr0, len0, atten_lim);\n return ret >>> 0;\n }\n\n let cachedFloat32Memory0 = null;\n\n function getFloat32Memory0() {\n if (cachedFloat32Memory0 === null || cachedFloat32Memory0.byteLength === 0) {\n cachedFloat32Memory0 = new Float32Array(wasm.memory.buffer);\n }\n return cachedFloat32Memory0;\n }\n\n function passArrayF32ToWasm0(arg, malloc) {\n const ptr = malloc(arg.length * 4, 4) >>> 0;\n getFloat32Memory0().set(arg, ptr / 4);\n WASM_VECTOR_LEN = arg.length;\n return ptr;\n }\n /**\n * Processes a chunk of samples.\n *\n * Args:\n * - df_state: Created via df_create()\n * - input: Input buffer of length df_get_frame_length()\n * - output: Output buffer of length df_get_frame_length()\n *\n * Returns:\n * - Local SNR of the current frame.\n * @param {number} st\n * @param {Float32Array} input\n * @returns {Float32Array}\n */\n function df_process_frame(st, input) {\n const ptr0 = passArrayF32ToWasm0(input, wasm.__wbindgen_malloc);\n const len0 = WASM_VECTOR_LEN;\n const ret = wasm.df_process_frame(st, ptr0, len0);\n return takeObject(ret);\n }\n\n function handleError(f, args) {\n try {\n return f.apply(this, args);\n } catch (e) {\n wasm.__wbindgen_exn_store(addHeapObject(e));\n }\n }\n\n (typeof FinalizationRegistry === 'undefined')\n ? { }\n : new FinalizationRegistry(ptr => wasm.__wbg_dfstate_free(ptr >>> 0));\n\n function __wbg_get_imports() {\n const imports = {};\n imports.wbg = {};\n imports.wbg.__wbindgen_object_drop_ref = function(arg0) {\n takeObject(arg0);\n };\n imports.wbg.__wbg_crypto_566d7465cdbb6b7a = function(arg0) {\n const ret = getObject(arg0).crypto;\n return addHeapObject(ret);\n };\n imports.wbg.__wbindgen_is_object = function(arg0) {\n const val = getObject(arg0);\n const ret = typeof(val) === 'object' && val !== null;\n return ret;\n };\n imports.wbg.__wbg_process_dc09a8c7d59982f6 = function(arg0) {\n const ret = getObject(arg0).process;\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_versions_d98c6400c6ca2bd8 = function(arg0) {\n const ret = getObject(arg0).versions;\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_node_caaf83d002149bd5 = function(arg0) {\n const ret = getObject(arg0).node;\n return addHeapObject(ret);\n };\n imports.wbg.__wbindgen_is_string = function(arg0) {\n const ret = typeof(getObject(arg0)) === 'string';\n return ret;\n };\n imports.wbg.__wbg_require_94a9da52636aacbf = function() { return handleError(function () {\n const ret = module.require;\n return addHeapObject(ret);\n }, arguments) };\n imports.wbg.__wbindgen_is_function = function(arg0) {\n const ret = typeof(getObject(arg0)) === 'function';\n return ret;\n };\n imports.wbg.__wbindgen_string_new = function(arg0, arg1) {\n const ret = getStringFromWasm0(arg0, arg1);\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_msCrypto_0b84745e9245cdf6 = function(arg0) {\n const ret = getObject(arg0).msCrypto;\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_randomFillSync_290977693942bf03 = function() { return handleError(function (arg0, arg1) {\n getObject(arg0).randomFillSync(takeObject(arg1));\n }, arguments) };\n imports.wbg.__wbg_getRandomValues_260cc23a41afad9a = function() { return handleError(function (arg0, arg1) {\n getObject(arg0).getRandomValues(getObject(arg1));\n }, arguments) };\n imports.wbg.__wbg_newnoargs_e258087cd0daa0ea = function(arg0, arg1) {\n const ret = new Function(getStringFromWasm0(arg0, arg1));\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_new_63b92bc8671ed464 = function(arg0) {\n const ret = new Uint8Array(getObject(arg0));\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_new_9efabd6b6d2ce46d = function(arg0) {\n const ret = new Float32Array(getObject(arg0));\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_buffer_12d079cc21e14bdb = function(arg0) {\n const ret = getObject(arg0).buffer;\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_newwithbyteoffsetandlength_aa4a17c33a06e5cb = function(arg0, arg1, arg2) {\n const ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0);\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_newwithlength_e9b4878cebadb3d3 = function(arg0) {\n const ret = new Uint8Array(arg0 >>> 0);\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_set_a47bac70306a19a7 = function(arg0, arg1, arg2) {\n getObject(arg0).set(getObject(arg1), arg2 >>> 0);\n };\n imports.wbg.__wbg_subarray_a1f73cd4b5b42fe1 = function(arg0, arg1, arg2) {\n const ret = getObject(arg0).subarray(arg1 >>> 0, arg2 >>> 0);\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_newwithbyteoffsetandlength_4a659d079a1650e0 = function(arg0, arg1, arg2) {\n const ret = new Float32Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0);\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_self_ce0dbfc45cf2f5be = function() { return handleError(function () {\n const ret = self.self;\n return addHeapObject(ret);\n }, arguments) };\n imports.wbg.__wbg_window_c6fb939a7f436783 = function() { return handleError(function () {\n const ret = window.window;\n return addHeapObject(ret);\n }, arguments) };\n imports.wbg.__wbg_globalThis_d1e6af4856ba331b = function() { return handleError(function () {\n const ret = globalThis.globalThis;\n return addHeapObject(ret);\n }, arguments) };\n imports.wbg.__wbg_global_207b558942527489 = function() { return handleError(function () {\n const ret = global.global;\n return addHeapObject(ret);\n }, arguments) };\n imports.wbg.__wbindgen_is_undefined = function(arg0) {\n const ret = getObject(arg0) === undefined;\n return ret;\n };\n imports.wbg.__wbg_call_27c0f87801dedf93 = function() { return handleError(function (arg0, arg1) {\n const ret = getObject(arg0).call(getObject(arg1));\n return addHeapObject(ret);\n }, arguments) };\n imports.wbg.__wbindgen_object_clone_ref = function(arg0) {\n const ret = getObject(arg0);\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_call_b3ca7c6051f9bec1 = function() { return handleError(function (arg0, arg1, arg2) {\n const ret = getObject(arg0).call(getObject(arg1), getObject(arg2));\n return addHeapObject(ret);\n }, arguments) };\n imports.wbg.__wbindgen_memory = function() {\n const ret = wasm.memory;\n return addHeapObject(ret);\n };\n imports.wbg.__wbindgen_throw = function(arg0, arg1) {\n throw new Error(getStringFromWasm0(arg0, arg1));\n };\n\n return imports;\n }\n\n function __wbg_finalize_init(instance, module) {\n wasm = instance.exports;\n cachedFloat32Memory0 = null;\n cachedUint8Memory0 = null;\n\n\n return wasm;\n }\n\n function initSync(module) {\n if (wasm !== undefined) return wasm;\n\n const imports = __wbg_get_imports();\n\n if (!(module instanceof WebAssembly.Module)) {\n module = new WebAssembly.Module(module);\n }\n\n const instance = new WebAssembly.Instance(module, imports);\n\n return __wbg_finalize_init(instance);\n }\n\n const WorkletMessageTypes = {\n SET_SUPPRESSION_LEVEL: 'SET_SUPPRESSION_LEVEL',\n SET_BYPASS: 'SET_BYPASS'\n };\n\n class DeepFilterAudioProcessor extends AudioWorkletProcessor {\n constructor(options) {\n super();\n this.dfModel = null;\n this.inputWritePos = 0;\n this.inputReadPos = 0;\n this.outputWritePos = 0;\n this.outputReadPos = 0;\n this.bypass = false;\n this.isInitialized = false;\n this.tempFrame = null;\n this.bufferSize = 8192;\n this.inputBuffer = new Float32Array(this.bufferSize);\n this.outputBuffer = new Float32Array(this.bufferSize);\n try {\n // Initialize WASM from pre-compiled module\n initSync(options.processorOptions.wasmModule);\n const modelBytes = new Uint8Array(options.processorOptions.modelBytes);\n const handle = df_create(modelBytes, options.processorOptions.suppressionLevel ?? 50);\n const frameLength = df_get_frame_length(handle);\n this.dfModel = { handle, frameLength };\n this.bufferSize = frameLength * 4;\n this.inputBuffer = new Float32Array(this.bufferSize);\n this.outputBuffer = new Float32Array(this.bufferSize);\n // Pre-allocate temp frame buffer for processing\n this.tempFrame = new Float32Array(frameLength);\n this.isInitialized = true;\n this.port.onmessage = (event) => {\n this.handleMessage(event.data);\n };\n }\n catch (error) {\n console.error('Failed to initialize DeepFilter in AudioWorklet:', error);\n this.isInitialized = false;\n }\n }\n handleMessage(data) {\n switch (data.type) {\n case WorkletMessageTypes.SET_SUPPRESSION_LEVEL:\n if (this.dfModel && typeof data.value === 'number') {\n const level = Math.max(0, Math.min(100, Math.floor(data.value)));\n df_set_atten_lim(this.dfModel.handle, level);\n }\n break;\n case WorkletMessageTypes.SET_BYPASS:\n this.bypass = Boolean(data.value);\n break;\n }\n }\n getInputAvailable() {\n return (this.inputWritePos - this.inputReadPos + this.bufferSize) % this.bufferSize;\n }\n getOutputAvailable() {\n return (this.outputWritePos - this.outputReadPos + this.bufferSize) % this.bufferSize;\n }\n process(inputList, outputList) {\n const sourceLimit = Math.min(inputList.length, outputList.length);\n const input = inputList[0]?.[0];\n if (!input) {\n return true;\n }\n // Passthrough mode - copy input to all output channels\n if (!this.isInitialized || !this.dfModel || this.bypass || !this.tempFrame) {\n for (let inputNum = 0; inputNum < sourceLimit; inputNum++) {\n const output = outputList[inputNum];\n const channelCount = output.length;\n for (let channelNum = 0; channelNum < channelCount; channelNum++) {\n output[channelNum].set(input);\n }\n }\n return true;\n }\n // Write input to ring buffer\n for (let i = 0; i < input.length; i++) {\n this.inputBuffer[this.inputWritePos] = input[i];\n this.inputWritePos = (this.inputWritePos + 1) % this.bufferSize;\n }\n const frameLength = this.dfModel.frameLength;\n while (this.getInputAvailable() >= frameLength) {\n // Extract frame from ring buffer\n for (let i = 0; i < frameLength; i++) {\n this.tempFrame[i] = this.inputBuffer[this.inputReadPos];\n this.inputReadPos = (this.inputReadPos + 1) % this.bufferSize;\n }\n const processed = df_process_frame(this.dfModel.handle, this.tempFrame);\n // Write to output ring buffer\n for (let i = 0; i < processed.length; i++) {\n this.outputBuffer[this.outputWritePos] = processed[i];\n this.outputWritePos = (this.outputWritePos + 1) % this.bufferSize;\n }\n }\n const outputAvailable = this.getOutputAvailable();\n if (outputAvailable >= 128) {\n for (let inputNum = 0; inputNum < sourceLimit; inputNum++) {\n const output = outputList[inputNum];\n const channelCount = output.length;\n for (let channelNum = 0; channelNum < channelCount; channelNum++) {\n const outputChannel = output[channelNum];\n let readPos = this.outputReadPos;\n for (let i = 0; i < 128; i++) {\n outputChannel[i] = this.outputBuffer[readPos];\n readPos = (readPos + 1) % this.bufferSize;\n }\n }\n }\n this.outputReadPos = (this.outputReadPos + 128) % this.bufferSize;\n }\n return true;\n }\n }\n registerProcessor('deepfilter-audio-processor', DeepFilterAudioProcessor);\n\n})();\n"; - -class DeepFilterNet3Core { - constructor(config = {}) { - this.assets = null; - this.workletNode = null; - this.isInitialized = false; - this.bypassEnabled = false; - this.config = { - sampleRate: config.sampleRate ?? 48000, - noiseReductionLevel: config.noiseReductionLevel ?? 50, - assetConfig: config.assetConfig - }; - this.assetLoader = getAssetLoader(config.assetConfig); - } - async initialize() { - if (this.isInitialized) - return; - // Fetch and compile WASM on main thread - const assetUrls = this.assetLoader.getAssetUrls(); - const [wasmBytes, modelBytes] = await Promise.all([ - this.assetLoader.fetchAsset(assetUrls.wasm), - this.assetLoader.fetchAsset(assetUrls.model) - ]); - // Compile WASM module - const wasmModule = await WebAssembly.compile(wasmBytes); - this.assets = { wasmModule, modelBytes }; - this.isInitialized = true; - } - async createAudioWorkletNode(audioContext) { - this.ensureInitialized(); - if (!this.assets) { - throw new Error('Assets not loaded'); - } - await createWorkletModule(audioContext, workletCode); - this.workletNode = new AudioWorkletNode(audioContext, 'deepfilter-audio-processor', { - processorOptions: { - wasmModule: this.assets.wasmModule, - modelBytes: this.assets.modelBytes, - suppressionLevel: this.config.noiseReductionLevel - } - }); - return this.workletNode; - } - setSuppressionLevel(level) { - if (!this.workletNode || typeof level !== 'number' || isNaN(level)) - return; - const clampedLevel = Math.max(0, Math.min(100, Math.floor(level))); - this.workletNode.port.postMessage({ - type: WorkletMessageTypes.SET_SUPPRESSION_LEVEL, - value: clampedLevel - }); - } - destroy() { - if (!this.isInitialized) - return; - if (this.workletNode) { - this.workletNode.disconnect(); - this.workletNode = null; - } - this.assets = null; - this.isInitialized = false; - } - isReady() { - return this.isInitialized && this.workletNode !== null; - } - setNoiseSuppressionEnabled(enabled) { - if (!this.workletNode) - return; - this.bypassEnabled = !enabled; - this.workletNode.port.postMessage({ - type: WorkletMessageTypes.SET_BYPASS, - value: !enabled - }); - } - isNoiseSuppressionEnabled() { - return !this.bypassEnabled; - } - ensureInitialized() { - if (!this.isInitialized) { - throw new Error('Processor not initialized. Call initialize() first.'); - } - } -} - -class DeepFilterNoiseFilterProcessor { - constructor(options = {}) { - this.name = 'deepfilternet3-noise-filter'; - this.audioContext = null; - this.sourceNode = null; - this.workletNode = null; - this.destination = null; - this.enabled = true; - this.init = async (opts) => { - const track = opts.track ?? opts.mediaStreamTrack; - if (!track) { - throw new Error('DeepFilterNoiseFilterProcessor.init: missing MediaStreamTrack'); - } - this.originalTrack = track; - await this.ensureGraph(); - }; - this.restart = async (opts) => { - const track = opts.track ?? opts.mediaStreamTrack; - if (track) { - this.originalTrack = track; - } - await this.ensureGraph(); - }; - this.setEnabled = async (enable) => { - this.enabled = enable; - this.processor.setNoiseSuppressionEnabled(enable); - return this.enabled; - }; - this.suspend = async () => { - if (this.audioContext && this.audioContext.state === 'running') { - await this.audioContext.suspend(); - } - }; - this.resume = async () => { - if (this.audioContext && this.audioContext.state === 'suspended') { - await this.audioContext.resume(); - } - }; - this.destroy = async () => { - await this.teardownGraph(); - this.processor.destroy(); - }; - const cfg = { - sampleRate: options.sampleRate ?? 48000, - noiseReductionLevel: options.noiseReductionLevel ?? 80, - assetConfig: options.assetConfig - }; - this.enabled = options.enabled ?? true; - this.processor = new DeepFilterNet3Core(cfg); - } - static isSupported() { - return typeof AudioContext !== 'undefined' && typeof WebAssembly !== 'undefined'; - } - setSuppressionLevel(level) { - this.processor.setSuppressionLevel(level); - } - isEnabled() { - return this.enabled; - } - isNoiseSuppressionEnabled() { - return this.processor.isNoiseSuppressionEnabled(); - } - async ensureGraph() { - if (!this.originalTrack) { - throw new Error('No source track'); - } - this.audioContext ?? (this.audioContext = new AudioContext({ sampleRate: 48000 })); - if (this.audioContext.state !== 'running') { - try { - await this.audioContext.resume(); - } - catch { - // Ignore resume errors - } - } - await this.processor.initialize(); - if (!this.workletNode) { - const node = await this.processor.createAudioWorkletNode(this.audioContext); - this.workletNode = node; - } - if (!this.destination) { - this.destination = this.audioContext.createMediaStreamDestination(); - this.processedTrack = this.destination.stream.getAudioTracks()[0]; - } - if (this.sourceNode) { - this.sourceNode.disconnect(); - } - this.sourceNode = this.audioContext.createMediaStreamSource(new MediaStream([this.originalTrack])); - this.sourceNode.connect(this.workletNode).connect(this.destination); - await this.setEnabled(this.enabled); - } - async teardownGraph() { - try { - if (this.workletNode) { - this.workletNode.disconnect(); - this.workletNode = null; - } - if (this.sourceNode) { - this.sourceNode.disconnect(); - this.sourceNode = null; - } - if (this.destination) { - this.destination.disconnect(); - this.destination = null; - } - if (this.audioContext) { - void this.audioContext.close(); - this.audioContext = null; - } - } - catch { - // Ignore disconnect errors - } - } -} -function DeepFilterNoiseFilter(options) { - return new DeepFilterNoiseFilterProcessor(options); -} - -export { AssetLoader, DeepFilterNet3Core, DeepFilterNoiseFilter, DeepFilterNoiseFilterProcessor, getAssetLoader }; diff --git a/static/vendor/deepfilternet3/v2/models/DeepFilterNet3_onnx.tar.gz b/static/vendor/deepfilternet3/v2/models/DeepFilterNet3_onnx.tar.gz deleted file mode 100644 index 1c4f4ff..0000000 Binary files a/static/vendor/deepfilternet3/v2/models/DeepFilterNet3_onnx.tar.gz and /dev/null differ diff --git a/static/vendor/deepfilternet3/v2/pkg/df_bg.wasm b/static/vendor/deepfilternet3/v2/pkg/df_bg.wasm deleted file mode 100644 index fac0f9e..0000000 Binary files a/static/vendor/deepfilternet3/v2/pkg/df_bg.wasm and /dev/null differ