diff --git a/src/chat.rs b/src/chat.rs deleted file mode 100644 index 10beca6..0000000 --- a/src/chat.rs +++ /dev/null @@ -1,118 +0,0 @@ -use crate::AppState; -use axum::extract::ws::{Message, WebSocket}; -use futures_util::{SinkExt, StreamExt}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use tokio::sync::{RwLock, mpsc}; -use uuid::Uuid; - -#[derive(Default)] -pub struct ChatHub { - // user_id -> sender - clients: RwLock>>, -} - -#[derive(Serialize, Clone)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum ServerEvent { - MessageCreated { - channel_id: Uuid, - message: serde_json::Value, - }, - DmCreated { - other_user_id: Uuid, - message: serde_json::Value, - }, - UserPresence { - user_id: Uuid, - online: bool, - }, -} - -#[derive(Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -enum ClientEvent { - // Currently no interactive client events for the general chat WS - Ping, -} - -impl ChatHub { - pub async fn add_client(&self, user_id: Uuid, tx: mpsc::UnboundedSender) { - let mut clients = self.clients.write().await; - clients.insert(user_id, tx); - } - - pub async fn remove_client(&self, user_id: Uuid) { - let mut clients = self.clients.write().await; - 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) { - let _ = tx.send(event); - } - } - - pub async fn broadcast_to_many(&self, user_ids: Vec, event: ServerEvent) { - let clients = self.clients.read().await; - for user_id in user_ids { - if let Some(tx) = clients.get(&user_id) { - let _ = tx.send(event.clone()); - } - } - } -} - -pub async fn handle_socket(state: AppState, socket: WebSocket, user_id: Uuid) { - let (mut ws_sender, mut ws_receiver) = socket.split(); - 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 { - let Ok(payload) = serde_json::to_string(&event) else { - continue; - }; - if ws_sender.send(Message::Text(payload.into())).await.is_err() { - break; - } - } - }); - - while let Some(Ok(msg)) = ws_receiver.next().await { - if let Message::Close(_) = msg { - break; - } - } - - 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/db.rs b/src/db.rs index cd6a234..df877ed 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,18 +1,15 @@ use anyhow::{Result, anyhow}; use chrono::{Duration, Utc}; use sea_orm::{ - ActiveModelTrait, ActiveValue::Set, ColumnTrait, Condition, DatabaseConnection, - DatabaseTransaction, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, + ActiveModelTrait, ActiveValue::Set, ColumnTrait, DatabaseConnection, + Condition, DatabaseTransaction, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, TransactionTrait, sea_query::OnConflict, }; use uuid::Uuid; use crate::{ entity::{channels, direct_messages, guild_members, guilds, invites, messages, users}, - models::{ - BasicUser, Channel, DmConversation, DmMessageWithAuthor, Guild, Invite, MessageWithAuthor, - User, - }, + models::{BasicUser, Channel, DmConversation, DmMessageWithAuthor, Guild, Invite, MessageWithAuthor, User}, }; pub const CHANNEL_KIND_TEXT: &str = "text"; @@ -45,10 +42,7 @@ pub async fn upsert_user_from_oidc( users::Column::DisplayName, users::Column::AvatarUrl, ]) - .value( - users::Column::UpdatedAt, - sea_orm::sea_query::Expr::current_timestamp(), - ) + .value(users::Column::UpdatedAt, sea_orm::sea_query::Expr::current_timestamp()) .to_owned(), ) .exec_with_returning(db) @@ -81,15 +75,6 @@ pub async fn list_guild_members(db: &DatabaseConnection, guild_id: Uuid) -> Resu .collect()) } -pub async fn list_guild_member_ids(db: &DatabaseConnection, guild_id: Uuid) -> Result> { - let rows = guild_members::Entity::find() - .filter(guild_members::Column::GuildId.eq(guild_id)) - .all(db) - .await?; - - Ok(rows.into_iter().map(|m| m.user_id).collect()) -} - pub async fn list_guilds_for_user(db: &DatabaseConnection, user_id: Uuid) -> Result> { let rows = guild_members::Entity::find() .filter(guild_members::Column::UserId.eq(user_id)) @@ -103,11 +88,7 @@ pub async fn list_guilds_for_user(db: &DatabaseConnection, user_id: Uuid) -> Res .collect()) } -pub async fn create_guild( - db: &DatabaseConnection, - owner_user_id: Uuid, - name: &str, -) -> Result { +pub async fn create_guild(db: &DatabaseConnection, owner_user_id: Uuid, name: &str) -> Result { let guild = guilds::Entity::insert(guilds::ActiveModel { id: Set(Uuid::new_v4()), name: Set(name.to_string()), @@ -123,12 +104,9 @@ pub async fn create_guild( ..Default::default() }) .on_conflict( - OnConflict::columns([ - guild_members::Column::GuildId, - guild_members::Column::UserId, - ]) - .do_nothing() - .to_owned(), + OnConflict::columns([guild_members::Column::GuildId, guild_members::Column::UserId]) + .do_nothing() + .to_owned(), ) .exec(db) .await?; @@ -198,12 +176,9 @@ pub async fn join_invite(db: &DatabaseConnection, code: &str, user_id: Uuid) -> ..Default::default() }) .on_conflict( - OnConflict::columns([ - guild_members::Column::GuildId, - guild_members::Column::UserId, - ]) - .do_nothing() - .to_owned(), + OnConflict::columns([guild_members::Column::GuildId, guild_members::Column::UserId]) + .do_nothing() + .to_owned(), ) .exec(&txn) .await?; @@ -240,18 +215,12 @@ pub async fn create_channel( Ok(map_channel(channel)) } -pub async fn get_channel_by_id( - db: &DatabaseConnection, - channel_id: Uuid, -) -> Result> { +pub async fn get_channel_by_id(db: &DatabaseConnection, channel_id: Uuid) -> Result> { let row = channels::Entity::find_by_id(channel_id).one(db).await?; Ok(row.map(map_channel)) } -pub async fn list_channels_for_guild( - db: &DatabaseConnection, - guild_id: Uuid, -) -> Result> { +pub async fn list_channels_for_guild(db: &DatabaseConnection, guild_id: Uuid) -> Result> { let channels = channels::Entity::find() .filter(channels::Column::GuildId.eq(guild_id)) .order_by_asc(channels::Column::CreatedAt) @@ -261,10 +230,7 @@ pub async fn list_channels_for_guild( Ok(channels.into_iter().map(map_channel).collect()) } -pub async fn list_voice_channels_for_guild( - db: &DatabaseConnection, - guild_id: Uuid, -) -> Result> { +pub async fn list_voice_channels_for_guild(db: &DatabaseConnection, guild_id: Uuid) -> Result> { let channels = channels::Entity::find() .filter(channels::Column::GuildId.eq(guild_id)) .filter(channels::Column::Kind.eq(CHANNEL_KIND_VOICE)) @@ -275,11 +241,7 @@ pub async fn list_voice_channels_for_guild( Ok(channels.into_iter().map(map_channel).collect()) } -pub async fn is_member_of_guild( - db: &DatabaseConnection, - guild_id: Uuid, - user_id: Uuid, -) -> Result { +pub async fn is_member_of_guild(db: &DatabaseConnection, guild_id: Uuid, user_id: Uuid) -> Result { let count = guild_members::Entity::find() .filter(guild_members::Column::GuildId.eq(guild_id)) .filter(guild_members::Column::UserId.eq(user_id)) @@ -289,10 +251,7 @@ pub async fn is_member_of_guild( Ok(count > 0) } -pub async fn guild_id_for_channel( - db: &DatabaseConnection, - channel_id: Uuid, -) -> Result> { +pub async fn guild_id_for_channel(db: &DatabaseConnection, channel_id: Uuid) -> Result> { let guild_id = channels::Entity::find_by_id(channel_id) .select_only() .column(channels::Column::GuildId) @@ -308,30 +267,18 @@ pub async fn create_message( channel_id: Uuid, author_user_id: Uuid, body: &str, -) -> Result { - let model = messages::Entity::insert(messages::ActiveModel { +) -> Result<()> { + messages::Entity::insert(messages::ActiveModel { id: Set(Uuid::new_v4()), channel_id: Set(channel_id), author_user_id: Set(author_user_id), body: Set(body.to_string()), ..Default::default() }) - .exec_with_returning(db) + .exec(db) .await?; - let user = users::Entity::find_by_id(author_user_id) - .one(db) - .await? - .ok_or_else(|| anyhow!("author not found"))?; - - Ok(MessageWithAuthor { - id: model.id, - channel_id: model.channel_id, - author_user_id: model.author_user_id, - author_display_name: user.display_name, - body: model.body, - created_at: model.created_at, - }) + Ok(()) } pub async fn list_messages( @@ -370,30 +317,18 @@ pub async fn create_direct_message( sender_user_id: Uuid, recipient_user_id: Uuid, body: &str, -) -> Result { - let model = direct_messages::Entity::insert(direct_messages::ActiveModel { +) -> Result<()> { + direct_messages::Entity::insert(direct_messages::ActiveModel { id: Set(Uuid::new_v4()), sender_user_id: Set(sender_user_id), recipient_user_id: Set(recipient_user_id), body: Set(body.to_string()), ..Default::default() }) - .exec_with_returning(db) + .exec(db) .await?; - let user = users::Entity::find_by_id(sender_user_id) - .one(db) - .await? - .ok_or_else(|| anyhow!("sender not found"))?; - - Ok(DmMessageWithAuthor { - id: model.id, - author_user_id: model.sender_user_id, - recipient_user_id: model.recipient_user_id, - author_display_name: user.display_name, - body: model.body, - created_at: model.created_at, - }) + Ok(()) } pub async fn list_direct_messages( @@ -450,10 +385,7 @@ pub async fn list_direct_messages( .collect()) } -pub async fn list_dm_conversations( - db: &DatabaseConnection, - current_user_id: Uuid, -) -> Result> { +pub async fn list_dm_conversations(db: &DatabaseConnection, current_user_id: Uuid) -> Result> { let rows = direct_messages::Entity::find() .filter( Condition::any() @@ -464,8 +396,7 @@ pub async fn list_dm_conversations( .all(db) .await?; - let mut latest_by_peer = - std::collections::HashMap::>::new(); + let mut latest_by_peer = std::collections::HashMap::>::new(); for row in rows { let peer_id = if row.sender_user_id == current_user_id { row.recipient_user_id @@ -565,10 +496,7 @@ fn validate_invite(invite: &invites::Model) -> Result<()> { Ok(()) } -async fn increment_invite_use_count( - txn: &DatabaseTransaction, - invite: invites::Model, -) -> Result<()> { +async fn increment_invite_use_count(txn: &DatabaseTransaction, invite: invites::Model) -> Result<()> { let next_count = invite.use_count + 1; let mut active: invites::ActiveModel = invite.into(); active.use_count = Set(next_count); diff --git a/src/handlers.rs b/src/handlers.rs index ffa363b..d86deec 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -11,7 +11,7 @@ use uuid::Uuid; use crate::{ AppState, auth::{self, ApiError, AuthUser}, - chat, db, voice, + db, voice, }; pub fn routes() -> Router { @@ -22,28 +22,17 @@ pub fn routes() -> Router { .route("/auth/logout", post(auth_logout)) .route("/me", get(me)) .route("/dms", get(list_dm_conversations)) - .route( - "/dms/{other_user_id}/messages", - get(list_dm_messages).post(send_dm_message), - ) - .route("/presence", get(presence_list)) + .route("/dms/{other_user_id}/messages", get(list_dm_messages).post(send_dm_message)) .route("/rtc-config", get(rtc_config)) .route("/guilds", get(list_guilds).post(create_guild)) .route("/guilds/{guild_id}/members", get(list_guild_members)) .route("/guilds/{guild_id}/channels", get(list_channels)) - .route( - "/guilds/{guild_id}/voice-presence", - get(guild_voice_presence), - ) + .route("/guilds/{guild_id}/voice-presence", get(guild_voice_presence)) .route("/guilds/{guild_id}/invites", post(create_invite)) .route("/invites/{code}/join", post(join_invite)) .route("/channels", post(create_channel)) - .route( - "/channels/{channel_id}/messages", - get(list_messages).post(send_message), - ) + .route("/channels/{channel_id}/messages", get(list_messages).post(send_message)) .route("/channels/{channel_id}/voice/ws", get(voice_ws)) - .route("/ws", get(chat_ws)) } pub async fn health() -> &'static str { @@ -153,10 +142,7 @@ async fn auth_callback( .map_err(|e| ApiError::bad_request(&format!("token exchange failed: {e}")))?; if !token_res.status().is_success() { - let text = token_res - .text() - .await - .unwrap_or_else(|_| "".to_string()); + let text = token_res.text().await.unwrap_or_else(|_| "".to_string()); return Err(ApiError::bad_request(&format!( "token exchange returned non-success: {text}" ))); @@ -176,10 +162,7 @@ async fn auth_callback( .map_err(|e| ApiError::bad_request(&format!("userinfo request failed: {e}")))?; if !userinfo_res.status().is_success() { - let text = userinfo_res - .text() - .await - .unwrap_or_else(|_| "".to_string()); + let text = userinfo_res.text().await.unwrap_or_else(|_| "".to_string()); return Err(ApiError::bad_request(&format!( "userinfo returned non-success: {text}" ))); @@ -202,12 +185,9 @@ async fn auth_callback( .await .map_err(|e| ApiError::internal(&format!("failed to persist user: {e}")))?; - let session_cookie = auth::new_session_cookie( - user.id, - &state.settings.session_secret, - state.settings.cookie_secure, - ) - .map_err(|e| ApiError::internal(&e.to_string()))?; + let session_cookie = + auth::new_session_cookie(user.id, &state.settings.session_secret, state.settings.cookie_secure) + .map_err(|e| ApiError::internal(&e.to_string()))?; let mut headers = HeaderMap::new(); headers.append( @@ -252,10 +232,7 @@ struct RtcIceServer { credential: Option, } -async fn rtc_config( - State(state): State, - _user: AuthUser, -) -> Result { +async fn rtc_config(State(state): State, _user: AuthUser) -> Result { let mut ice_servers = Vec::new(); if !state.settings.stun_urls.is_empty() { @@ -294,14 +271,6 @@ 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, @@ -472,13 +441,9 @@ async fn create_channel( return Err(ApiError::bad_request("channel name must be 1..64 chars")); } - let kind = body - .kind - .unwrap_or_else(|| db::CHANNEL_KIND_TEXT.to_string()); + let kind = body.kind.unwrap_or_else(|| db::CHANNEL_KIND_TEXT.to_string()); if kind != db::CHANNEL_KIND_TEXT && kind != db::CHANNEL_KIND_VOICE { - return Err(ApiError::bad_request( - "channel kind must be 'text' or 'voice'", - )); + return Err(ApiError::bad_request("channel kind must be 'text' or 'voice'")); } let channel = db::create_channel(&state.db, body.guild_id, trimmed, &kind) @@ -532,31 +497,10 @@ async fn send_message( return Err(ApiError::bad_request("message body must be 1..4000 chars")); } - let message = db::create_message(&state.db, channel_id, user.id, content) + db::create_message(&state.db, channel_id, user.id, content) .await .map_err(|e| ApiError::internal(&format!("failed to create message: {e}")))?; - // Broadcast to all guild members - let guild_id = db::guild_id_for_channel(&state.db, channel_id) - .await - .map_err(|e| ApiError::internal(&format!("channel lookup failed: {e}")))? - .ok_or_else(|| ApiError::internal("channel not found after creation"))?; - - let members = db::list_guild_member_ids(&state.db, guild_id) - .await - .map_err(|e| ApiError::internal(&format!("failed to list guild members: {e}")))?; - - state - .chat - .broadcast_to_many( - members, - chat::ServerEvent::MessageCreated { - channel_id, - message: serde_json::to_value(&message).unwrap_or_default(), - }, - ) - .await; - Ok((StatusCode::CREATED, Json(SendMessageResponse { ok: true }))) } @@ -613,32 +557,10 @@ async fn send_dm_message( return Err(ApiError::bad_request("message body must be 1..4000 chars")); } - let message = db::create_direct_message(&state.db, user.id, other_user_id, content) + db::create_direct_message(&state.db, user.id, other_user_id, content) .await .map_err(|e| ApiError::internal(&format!("failed to create dm message: {e}")))?; - // Broadcast to both users - state - .chat - .broadcast_to_user( - user.id, - chat::ServerEvent::DmCreated { - other_user_id, - message: serde_json::to_value(&message).unwrap_or_default(), - }, - ) - .await; - state - .chat - .broadcast_to_user( - other_user_id, - chat::ServerEvent::DmCreated { - other_user_id: user.id, - message: serde_json::to_value(&message).unwrap_or_default(), - }, - ) - .await; - Ok((StatusCode::CREATED, Json(SendMessageResponse { ok: true }))) } @@ -659,27 +581,15 @@ async fn voice_ws( })?; if channel.kind != db::CHANNEL_KIND_VOICE { - return Err(ApiError::bad_request( - "voice websocket requires a voice channel", - )); + return Err(ApiError::bad_request("voice websocket requires a voice channel")); } - Ok(ws.on_upgrade(move |socket| voice::handle_socket(state, socket, channel_id, user.id))) + Ok(ws.on_upgrade(move |socket| { + voice::handle_socket(state, socket, channel_id, user.id) + })) } -async fn chat_ws( - ws: WebSocketUpgrade, - State(state): State, - user: AuthUser, -) -> Result { - Ok(ws.on_upgrade(move |socket| chat::handle_socket(state, socket, user.id))) -} - -async fn ensure_guild_member( - state: &AppState, - guild_id: Uuid, - user_id: Uuid, -) -> Result<(), ApiError> { +async fn ensure_guild_member(state: &AppState, guild_id: Uuid, user_id: Uuid) -> Result<(), ApiError> { let is_member = db::is_member_of_guild(&state.db, guild_id, user_id) .await .map_err(|e| ApiError::internal(&format!("membership check failed: {e}")))?; @@ -694,11 +604,7 @@ async fn ensure_guild_member( Ok(()) } -async fn ensure_channel_member( - state: &AppState, - channel_id: Uuid, - user_id: Uuid, -) -> Result<(), ApiError> { +async fn ensure_channel_member(state: &AppState, channel_id: Uuid, user_id: Uuid) -> Result<(), ApiError> { let guild_id = db::guild_id_for_channel(&state.db, channel_id) .await .map_err(|e| ApiError::internal(&format!("channel lookup failed: {e}")))? diff --git a/src/main.rs b/src/main.rs index 53ea6f2..fdeaeb9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ mod auth; -mod chat; mod config; mod db; mod entity; @@ -25,7 +24,6 @@ pub struct AppState { pub settings: Arc, pub http: reqwest::Client, pub voice: Arc, - pub chat: Arc, } #[tokio::main] @@ -48,7 +46,6 @@ async fn main() -> anyhow::Result<()> { settings, http: reqwest::Client::new(), voice: Arc::new(VoiceHub::default()), - chat: Arc::new(chat::ChatHub::default()), }; let port = state.settings.port; diff --git a/static/app.js b/static/app.js index 8f4347f..6bbe240 100644 --- a/static/app.js +++ b/static/app.js @@ -16,14 +16,13 @@ const state = { localStream: null, rawStream: null, audioContext: null, + denoiserNode: null, + deepFilterCore: null, peerConnections: new Map(), muted: false, iceServers: [{ urls: "stun:stun.l.google.com:19302" }], }, voicePresencePollId: null, - chatWs: null, - lastMessageId: null, - onlineUsers: new Set(), }; const el = { @@ -141,55 +140,10 @@ function formatDate(isoString) { return d.toLocaleDateString(); } -let audioCtx = null; +const DEEPFILTERNET_LIB_PATH = "/static/vendor/deepfilternet3/index.esm.js"; +const NSNET2_COMPAT_SUPPRESSION = 56; -// 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)(); - if (audioCtx.state === 'suspended') audioCtx.resume(); - - const sounds = { - message: { freq: 880, type: 'sine', duration: 0.1, volume: 0.1 }, - dm: { freq: [660, 880], type: 'sine', duration: 0.15, volume: 0.1 }, - join: { freq: [440, 880], type: 'sine', duration: 0.2, volume: 0.1 }, - leave: { freq: [880, 440], type: 'sine', duration: 0.2, volume: 0.1 }, - 'peer-join': { freq: [660, 990], type: 'sine', duration: 0.15, volume: 0.05 }, - 'peer-leave': { freq: [990, 660], type: 'sine', duration: 0.15, volume: 0.05 }, - }; - - const s = sounds[type]; - if (!s) return; - - const osc = audioCtx.createOscillator(); - const gain = audioCtx.createGain(); - - osc.type = s.type; - if (Array.isArray(s.freq)) { - osc.frequency.setValueAtTime(s.freq[0], audioCtx.currentTime); - osc.frequency.exponentialRampToValueAtTime(s.freq[1], audioCtx.currentTime + s.duration); - } else { - osc.frequency.setValueAtTime(s.freq, audioCtx.currentTime); - } - - gain.gain.setValueAtTime(s.volume, audioCtx.currentTime); - gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + s.duration); - - osc.connect(gain); - gain.connect(audioCtx.destination); - - osc.start(); - osc.stop(audioCtx.currentTime + s.duration); - } catch (err) { - console.warn("playSound failed", err); - } -} +let deepFilterLibPromise = null; const LAST_GUILD_STORAGE_KEY = "chattz:lastGuildId"; // --- Renderers --- @@ -229,7 +183,7 @@ function renderChannels() { for (const channel of state.channels) { const row = document.createElement("button"); row.className = `channel-row ${(channel.kind === 'text' && state.selectedTextChannelId === channel.id) || - (channel.kind === 'voice' && state.selectedVoiceChannelId === channel.id) ? "active" : "" + (channel.kind === 'voice' && state.selectedVoiceChannelId === channel.id) ? "active" : "" }`; const iconName = channel.kind === 'text' ? 'hash' : 'volume-2'; @@ -283,16 +237,9 @@ 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; @@ -344,24 +291,16 @@ function renderMessages(messages) { lastTime = mDate; } - if (messages.length > 0) { - state.lastMessageId = messages[0].id; - } - el.messageList.scrollTop = el.messageList.scrollHeight; } 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"; @@ -483,49 +422,6 @@ function startVoicePresencePolling() { }, 3000); } -function initChatWs() { - if (state.chatWs) state.chatWs.close(); - const protocol = location.protocol === "https:" ? "wss:" : "ws:"; - const wsUrl = `${protocol}//${location.host}/ws`; - const ws = new WebSocket(wsUrl); - state.chatWs = ws; - - 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) { - api(`/channels/${state.selectedTextChannelId}/messages?limit=100`).then(renderMessages); - } - } 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); - 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(); - } - }; - - ws.onclose = () => { - console.log("Chat WS closed, reconnecting..."); - setTimeout(initChatWs, 3000); - }; -} - // --- Voice --- function getVoiceWsUrl(channelId) { @@ -538,7 +434,40 @@ function shouldInitiateOffer(peerId) { return state.me.id > peerId; } +async function loadDeepFilterLib() { + if (!deepFilterLibPromise) { + deepFilterLibPromise = import(DEEPFILTERNET_LIB_PATH); + } + return deepFilterLibPromise; +} + +async function buildDenoiserNode(audioContext) { + if (!audioContext.audioWorklet) { + throw new Error("AudioWorklet is not supported in this browser"); + } + + const df = await loadDeepFilterLib(); + const core = new df.DeepFilterNet3Core({ + sampleRate: 48000, + noiseReductionLevel: NSNET2_COMPAT_SUPPRESSION, + assetConfig: { + cdnUrl: "/static/vendor/deepfilternet3", + }, + }); + await core.initialize(); + const workletNode = await core.createAudioWorkletNode(audioContext); + state.voice.deepFilterCore = core; + return workletNode; +} + function stopAndClearAudioPipeline() { + if (state.voice.deepFilterCore) { + state.voice.deepFilterCore.destroy(); + state.voice.deepFilterCore = null; + } + if (state.voice.denoiserNode && typeof state.voice.denoiserNode.destroy === "function") { + state.voice.denoiserNode.destroy(); + } if (state.voice.localStream) { for (const track of state.voice.localStream.getTracks()) track.stop(); } @@ -551,6 +480,8 @@ function stopAndClearAudioPipeline() { state.voice.localStream = null; state.voice.rawStream = null; state.voice.audioContext = null; + state.voice.denoiserNode = null; + state.voice.deepFilterCore = null; } async function buildAudioPipeline(rawStream) { @@ -561,8 +492,14 @@ async function buildAudioPipeline(rawStream) { let head = source; + const denoiserNode = await buildDenoiserNode(audioContext); + if (denoiserNode) { + head.connect(denoiserNode); + head = denoiserNode; + } head.connect(destination); + state.voice.denoiserNode = denoiserNode; return destination.stream; } @@ -684,7 +621,6 @@ async function joinVoice() { const channel = state.channels.find(c => c.id === state.selectedVoiceChannelId); el.vcChannelName.textContent = channel ? channel.name : "Voice"; el.voiceConnection.classList.remove("hidden"); - playSound('join'); refreshVoicePresence().catch(() => { }); }; @@ -695,10 +631,8 @@ async function joinVoice() { if (shouldInitiateOffer(peer.user_id)) await sendOffer(peer.user_id); } } else if (msg.type === "peer_joined") { - playSound('peer-join'); if (shouldInitiateOffer(msg.user_id)) await sendOffer(msg.user_id); } else if (msg.type === "peer_left") { - playSound('peer-leave'); const pc = state.voice.peerConnections.get(msg.user_id); if (pc) { pc.close(); @@ -718,7 +652,6 @@ async function joinVoice() { stopAndClearAudioPipeline(); state.voice.joinedChannelId = null; state.voice.ws = null; - playSound('leave'); refreshVoicePresence().catch(() => { }); }; } @@ -944,11 +877,6 @@ 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]; @@ -965,7 +893,6 @@ async function init() { updateHeaderLabels(); } - initChatWs(); startVoicePresencePolling(); lucide.createIcons(); } catch (err) { diff --git a/static/styles.css b/static/styles.css index c1b9d6f..012ceea 100644 --- a/static/styles.css +++ b/static/styles.css @@ -7,24 +7,22 @@ --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; @@ -36,46 +34,15 @@ body { } /* Scrollbars */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} +::-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-track { - background: transparent; -} +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-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; -} +.hidden { display: none !important; } /* Auth Screen */ .auth-screen { @@ -85,16 +52,14 @@ select { 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; @@ -107,17 +72,8 @@ select { 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); @@ -127,10 +83,7 @@ select { font-weight: 600; font-size: 16px; } - -#login-btn:hover { - background: var(--brand-hover); -} +#login-btn:hover { background: var(--brand-hover); } /* Main Layout */ .shell { @@ -151,11 +104,7 @@ select { 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; @@ -163,8 +112,7 @@ select { padding: 6px 0; } -.brand, -.guild-pill { +.brand, .guild-pill { width: 48px; height: 48px; border-radius: 50%; @@ -176,16 +124,8 @@ select { 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; @@ -195,8 +135,7 @@ select { border-radius: 1px; } -.guild-pill:hover, -.guild-pill.active { +.guild-pill:hover, .guild-pill.active { border-radius: 16px; background: var(--brand); color: #fff; @@ -212,23 +151,11 @@ select { border-radius: 0 4px 4px 0; transition: all 0.2s ease; } +.guild-pill:hover::before { height: 20px; } +.guild-pill.active::before { height: 40px; } -.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; -} +.action-pill { color: var(--green); } +.action-pill:hover { background: var(--green); color: #fff; } /* Channel Sidebar */ .channel-sidebar { @@ -244,15 +171,11 @@ select { 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; @@ -262,13 +185,11 @@ select { overflow: hidden; text-overflow: ellipsis; } - .sidebar-header-actions { display: flex; align-items: center; gap: 8px; } - .header-action-btn { width: 24px; height: 24px; @@ -277,27 +198,18 @@ select { 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-group { - margin-bottom: 20px; -} +.sidebar-scroll { flex: 1; overflow-y: auto; padding-top: 12px; } +.sidebar-group { margin-bottom: 20px; } .group-title { padding: 0 8px 0 2px; display: flex; @@ -309,21 +221,9 @@ select { 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; @@ -333,24 +233,13 @@ select { display: grid; place-items: center; } - .add-btn:hover { opacity: 1; background: var(--bg-modifier-hover); } +.add-btn i { width: 16px; height: 16px; } -.add-btn i { - width: 16px; - height: 16px; -} - -.channel-list { - padding: 0 8px; - display: flex; - flex-direction: column; - gap: 2px; -} - +.channel-list { padding: 0 8px; display: flex; flex-direction: column; gap: 2px; } .channel-row { display: flex; align-items: center; @@ -360,28 +249,12 @@ select { 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; @@ -390,15 +263,9 @@ select { gap: 8px; height: 52px; } +.user-panel:hover { background: var(--bg-modifier-hover); } -.user-panel:hover { - background: var(--bg-modifier-hover); -} - -.avatar-wrapper { - position: relative; -} - +.avatar-wrapper { position: relative; } .avatar { width: 32px; height: 32px; @@ -410,7 +277,6 @@ select { font-weight: 700; font-size: 14px; } - .status-dot { position: absolute; bottom: -2px; @@ -420,16 +286,9 @@ select { border-radius: 50%; border: 3px solid var(--bg-secondary); } +.status-dot.online { background: var(--green); } -.status-dot.online { - background: var(--green); -} - -.user-info { - flex: 1; - min-width: 0; -} - +.user-info { flex: 1; min-width: 0; } .display-name { font-size: 14px; font-weight: 600; @@ -438,17 +297,9 @@ select { overflow: hidden; text-overflow: ellipsis; } +.user-status { font-size: 12px; color: var(--text-muted); } -.user-status { - font-size: 12px; - color: var(--text-muted); -} - -.user-actions { - display: flex; - gap: 2px; -} - +.user-actions { display: flex; gap: 2px; } .user-actions button { width: 32px; height: 32px; @@ -457,57 +308,21 @@ select { 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; @@ -516,11 +331,7 @@ select { 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 { @@ -536,21 +347,11 @@ select { 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); -} - -.header-icon { - color: var(--text-muted); - width: 24px; -} - -.chat-header h3 { - margin: 0; - font-size: 16px; - font-weight: 700; - color: var(--text-strong); + 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); } .message-list { flex: 1; @@ -564,10 +365,7 @@ select { 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; @@ -581,77 +379,36 @@ select { 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-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-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 { @@ -659,13 +416,7 @@ select { 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; @@ -675,12 +426,7 @@ select { 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; @@ -693,67 +439,23 @@ select { 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); @@ -761,17 +463,8 @@ select { 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; @@ -779,7 +472,6 @@ select { color: var(--text-muted); margin-bottom: 8px; } - .form-item input { width: 100%; padding: 10px; @@ -793,17 +485,8 @@ select { 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; @@ -811,28 +494,12 @@ select { 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; @@ -842,45 +509,21 @@ select { 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; @@ -898,50 +541,39 @@ select { 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; } @@ -951,28 +583,25 @@ select { 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 new file mode 100644 index 0000000..7ad3ee9 --- /dev/null +++ b/static/vendor/deepfilternet3/index.esm.js @@ -0,0 +1,249 @@ +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 new file mode 100644 index 0000000..1c4f4ff Binary files /dev/null and b/static/vendor/deepfilternet3/v2/models/DeepFilterNet3_onnx.tar.gz differ diff --git a/static/vendor/deepfilternet3/v2/pkg/df_bg.wasm b/static/vendor/deepfilternet3/v2/pkg/df_bg.wasm new file mode 100644 index 0000000..fac0f9e Binary files /dev/null and b/static/vendor/deepfilternet3/v2/pkg/df_bg.wasm differ