feat(voice): add noise suppression backends and rollout (#2706)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-11 22:24:05 +02:00
1 parent b30a4f5d14
commit 172791316b
97 files changed
+10877 -3157

No files matched your search

@@ -18,6 +18,10 @@ pub struct InstanceConfigResponse {
pub integrations: InstanceIntegrationsResponse,
#[serde(default)]
pub media: InstanceMediaResponse,
#[serde(default)]
pub voice_noise_suppression: VoiceNoiseSuppressionConfigResponse,
#[serde(default)]
pub experiment_delivery: ExperimentDeliveryConfigResponse,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -436,6 +440,192 @@ impl VoiceE2eeScope {
}
}
pub const VOICE_NS_MAX_TARGETED_USERS: usize = 1_000;
pub const VOICE_NS_MAX_GUILD_OVERRIDES: usize = 200;
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum NoiseSuppressionBackend {
None,
#[default]
Standard,
Gate,
Speex,
Rnnoise,
Gtcrn,
DeepFilter,
}
impl NoiseSuppressionBackend {
pub const ALL: [Self; 7] = [
Self::None,
Self::Standard,
Self::Gate,
Self::Speex,
Self::Rnnoise,
Self::Gtcrn,
Self::DeepFilter,
];
pub fn as_str(&self) -> &'static str {
match self {
Self::None => "none",
Self::Standard => "standard",
Self::Gate => "gate",
Self::Speex => "speex",
Self::Rnnoise => "rnnoise",
Self::Gtcrn => "gtcrn",
Self::DeepFilter => "deep_filter",
}
}
pub fn label(&self) -> &'static str {
match self {
Self::None => "None (pass-through)",
Self::Standard => "Standard (WebRTC)",
Self::Gate => "Noise gate",
Self::Speex => "Speex",
Self::Rnnoise => "RNNoise",
Self::Gtcrn => "GTCRN",
Self::DeepFilter => "DeepFilterNet",
}
}
pub fn from_value(value: &str) -> Option<Self> {
Self::ALL
.into_iter()
.find(|backend| backend.as_str() == value)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct VoiceNoiseSuppressionGuildOverride {
pub guild_id: String,
pub backend: NoiseSuppressionBackend,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct VoiceNoiseSuppressionConfigResponse {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub config_version: u64,
#[serde(default)]
pub default_backend: NoiseSuppressionBackend,
#[serde(default = "default_voice_noise_suppression_enabled_backends")]
pub enabled_backends: Vec<NoiseSuppressionBackend>,
#[serde(default = "default_voice_noise_suppression_allow_user_override")]
pub allow_user_override: bool,
#[serde(default)]
pub rollout_basis_points: u32,
#[serde(default = "default_voice_noise_suppression_rollout_salt")]
pub rollout_salt: String,
#[serde(default)]
pub included_user_ids: Vec<String>,
#[serde(default)]
pub excluded_user_ids: Vec<String>,
#[serde(default)]
pub guild_overrides: Vec<VoiceNoiseSuppressionGuildOverride>,
#[serde(default)]
pub stereo_enabled: bool,
#[serde(default = "default_voice_noise_suppression_strength")]
pub suppression_strength: u32,
}
impl Default for VoiceNoiseSuppressionConfigResponse {
fn default() -> Self {
Self {
enabled: false,
config_version: 0,
default_backend: NoiseSuppressionBackend::Standard,
enabled_backends: default_voice_noise_suppression_enabled_backends(),
allow_user_override: default_voice_noise_suppression_allow_user_override(),
rollout_basis_points: 0,
rollout_salt: default_voice_noise_suppression_rollout_salt(),
included_user_ids: Vec::new(),
excluded_user_ids: Vec::new(),
guild_overrides: Vec::new(),
stereo_enabled: false,
suppression_strength: default_voice_noise_suppression_strength(),
}
}
}
fn default_voice_noise_suppression_enabled_backends() -> Vec<NoiseSuppressionBackend> {
NoiseSuppressionBackend::ALL.to_vec()
}
fn default_voice_noise_suppression_allow_user_override() -> bool {
true
}
fn default_voice_noise_suppression_rollout_salt() -> String {
"voice-ns-v1".to_owned()
}
fn default_voice_noise_suppression_strength() -> u32 {
80
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct VoiceNoiseSuppressionConfigUpdateRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_backend: Option<NoiseSuppressionBackend>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled_backends: Option<Vec<NoiseSuppressionBackend>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_user_override: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rollout_basis_points: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rollout_salt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub included_user_ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub excluded_user_ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub guild_overrides: Option<Vec<VoiceNoiseSuppressionGuildOverride>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stereo_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub suppression_strength: Option<u32>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ExperimentDeliveryConfigResponse {
#[serde(default = "default_experiment_poll_interval_seconds")]
pub poll_interval_seconds: u64,
#[serde(default = "default_experiment_poll_jitter_percent")]
pub poll_jitter_percent: u32,
}
impl Default for ExperimentDeliveryConfigResponse {
fn default() -> Self {
Self {
poll_interval_seconds: default_experiment_poll_interval_seconds(),
poll_jitter_percent: default_experiment_poll_jitter_percent(),
}
}
}
fn default_experiment_poll_interval_seconds() -> u64 {
300
}
fn default_experiment_poll_jitter_percent() -> u32 {
15
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct ExperimentDeliveryConfigUpdateRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub poll_interval_seconds: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub poll_jitter_percent: Option<u32>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct InstanceRegistrationResponse {
pub mode: RegistrationMode,
@@ -525,6 +715,10 @@ pub struct InstanceConfigUpdateRequest {
pub integrations: Option<InstanceIntegrationsUpdateRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub media: Option<InstanceMediaUpdateRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub voice_noise_suppression: Option<VoiceNoiseSuppressionConfigUpdateRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub experiment_delivery: Option<ExperimentDeliveryConfigUpdateRequest>,
}
#[derive(Clone, Debug, Default, Serialize)]
@@ -826,3 +1020,65 @@ pub struct CreateRegistrationUrlResponse {
pub code: String,
pub url: String,
}
#[cfg(test)]
mod tests {
use super::*;
const VOICE_NOISE_SUPPRESSION_SCHEMA: &str = include_str!(
"../../../../packages/schema/src/domains/admin/VoiceNoiseSuppressionSchemas.ts"
);
fn schema_backend_ids() -> Vec<String> {
let (_, rest) = VOICE_NOISE_SUPPRESSION_SCHEMA
.split_once("export const VOICE_NOISE_SUPPRESSION_BACKENDS = [")
.expect("backend list start");
let (block, _) = rest.split_once(']').expect("backend list end");
block
.split(',')
.map(|entry| entry.trim().trim_matches('\'').to_owned())
.filter(|entry| !entry.is_empty())
.collect()
}
#[test]
fn noise_suppression_backend_ids_are_the_documented_list() {
assert_eq!(
NoiseSuppressionBackend::ALL
.iter()
.map(NoiseSuppressionBackend::as_str)
.collect::<Vec<_>>(),
vec![
"none",
"standard",
"gate",
"speex",
"rnnoise",
"gtcrn",
"deep_filter"
]
);
}
#[test]
fn noise_suppression_backend_ids_match_the_schema_contract() {
assert_eq!(
NoiseSuppressionBackend::ALL
.iter()
.map(|backend| backend.as_str().to_owned())
.collect::<Vec<_>>(),
schema_backend_ids()
);
}
#[test]
fn noise_suppression_backend_round_trips_every_id() {
for backend in NoiseSuppressionBackend::ALL {
assert_eq!(
NoiseSuppressionBackend::from_value(backend.as_str()),
Some(backend)
);
}
assert_eq!(NoiseSuppressionBackend::from_value("deepfilter"), None);
}
}
+529 -3
View File
@@ -7,7 +7,8 @@ use crate::{
AppBrandingConfigUpdateRequest, AppLegalConfigUpdateRequest,
AppPublicConfigUpdateRequest, AppRegistrationConfigUpdateRequest,
AppSetupConfigUpdateRequest, CreateRegistrationUrlRequest,
DeferredPhoneGateUpdateRequest, GatewayRolloutConfigUpdateRequest, GatewayRolloutMode,
DeferredPhoneGateUpdateRequest, ExperimentDeliveryConfigUpdateRequest,
GatewayRolloutConfigUpdateRequest, GatewayRolloutMode,
InstanceAttachmentDecayUpdateRequest, InstanceBlueskyIntegrationUpdateRequest,
InstanceBlueskyKeyIntegrationUpdateRequest, InstanceCaptchaIntegrationUpdateRequest,
InstanceConfigUpdateRequest, InstanceEmailIntegrationUpdateRequest,
@@ -16,8 +17,10 @@ use crate::{
InstanceMediaUpdateRequest, InstancePolicyUpdateRequest,
InstanceRegistrationConfigUpdateRequest, InstanceServicesUpdateRequest,
InstanceYoutubeIntegrationUpdateRequest, LimitConfigUpdateRequest, LimitRule,
LimitRuleFilters, PremiumMode, RegistrationMode, SsoConfigUpdateRequest,
VoiceE2eeScope,
LimitRuleFilters, NoiseSuppressionBackend, PremiumMode, RegistrationMode,
SsoConfigUpdateRequest, VOICE_NS_MAX_GUILD_OVERRIDES, VOICE_NS_MAX_TARGETED_USERS,
VoiceE2eeScope, VoiceNoiseSuppressionConfigUpdateRequest,
VoiceNoiseSuppressionGuildOverride,
},
},
config::AdminConfig,
@@ -200,6 +203,14 @@ pub async fn instance_config_post(
let update = build_media_update(&form);
instance_config_result(client.update_instance_config(&update).await)
}
"update_voice_noise_suppression" => {
let update = build_voice_noise_suppression_update(&form);
instance_config_result(client.update_instance_config(&update).await)
}
"update_experiment_delivery" => {
let update = build_experiment_delivery_update(&form);
instance_config_result(client.update_instance_config(&update).await)
}
"test_smtp" => match build_smtp_test_request(&form) {
Ok(request) => match client.test_instance_smtp_config(&request).await {
Ok(response) if response.ok => FlashData::success("SMTP connection verified"),
@@ -407,6 +418,8 @@ fn build_sso_update(form: &MultiValueForm) -> InstanceConfigUpdateRequest {
policy: None,
integrations: None,
media: None,
voice_noise_suppression: None,
experiment_delivery: None,
}
}
@@ -443,6 +456,183 @@ fn build_gateway_rollout_update(form: &MultiValueForm) -> InstanceConfigUpdateRe
policy: None,
integrations: None,
media: None,
voice_noise_suppression: None,
experiment_delivery: None,
}
}
const VOICE_NS_ROLLOUT_BASIS_POINTS_MAX: u32 = 10_000;
const VOICE_NS_SUPPRESSION_STRENGTH_MAX: u32 = 100;
const VOICE_NS_MAX_ROLLOUT_SALT_CHARS: usize = 64;
const VOICE_NS_MAX_SNOWFLAKE_LENGTH: usize = 20;
const EXPERIMENT_MIN_POLL_INTERVAL_SECONDS: u64 = 60;
const EXPERIMENT_MAX_POLL_INTERVAL_SECONDS: u64 = 86_400;
const EXPERIMENT_DEFAULT_POLL_INTERVAL_SECONDS: u64 = 300;
const EXPERIMENT_MAX_POLL_JITTER_PERCENT: u32 = 50;
const EXPERIMENT_DEFAULT_POLL_JITTER_PERCENT: u32 = 15;
fn parse_clamped_form_number<T>(
form: &MultiValueForm,
key: &str,
empty_value: T,
min: T,
max: T,
) -> Option<T>
where
T: std::str::FromStr + Ord,
{
let raw = form.first(key)?.trim();
let value = if raw.is_empty() {
empty_value
} else {
raw.parse::<T>().unwrap_or(empty_value)
};
Some(value.clamp(min, max))
}
fn parse_voice_noise_suppression_rollout_salt(form: &MultiValueForm) -> Option<String> {
let salt: String = form
.clean("voice_ns_rollout_salt")?
.chars()
.take(VOICE_NS_MAX_ROLLOUT_SALT_CHARS)
.collect();
clean_string(&salt)
}
fn is_voice_noise_suppression_snowflake(value: &str) -> bool {
!value.is_empty()
&& value.len() <= VOICE_NS_MAX_SNOWFLAKE_LENGTH
&& value.bytes().all(|byte| byte.is_ascii_digit())
}
fn parse_voice_noise_suppression_user_ids(value: &str) -> Vec<String> {
let mut ids: Vec<String> = Vec::new();
for candidate in value.split([',', '\n', '\r']) {
let candidate = candidate.trim();
if !is_voice_noise_suppression_snowflake(candidate) {
continue;
}
if ids.iter().any(|existing| existing == candidate) {
continue;
}
ids.push(candidate.to_owned());
if ids.len() == VOICE_NS_MAX_TARGETED_USERS {
break;
}
}
ids
}
fn parse_voice_noise_suppression_guild_overrides(
value: &str,
) -> Vec<VoiceNoiseSuppressionGuildOverride> {
let mut overrides: Vec<VoiceNoiseSuppressionGuildOverride> = Vec::new();
for line in value.lines() {
let Some((guild_id, backend)) = line.split_once('=') else {
continue;
};
let guild_id = guild_id.trim();
if !is_voice_noise_suppression_snowflake(guild_id) {
continue;
}
if overrides
.iter()
.any(|existing| existing.guild_id == guild_id)
{
continue;
}
let Some(backend) = NoiseSuppressionBackend::from_value(backend.trim()) else {
continue;
};
overrides.push(VoiceNoiseSuppressionGuildOverride {
guild_id: guild_id.to_owned(),
backend,
});
if overrides.len() == VOICE_NS_MAX_GUILD_OVERRIDES {
break;
}
}
overrides
}
fn build_voice_noise_suppression_update(form: &MultiValueForm) -> InstanceConfigUpdateRequest {
let selected =
form.list_values_any(&["voice_ns_enabled_backends[]", "voice_ns_enabled_backends"]);
let enabled_backends = NoiseSuppressionBackend::ALL
.into_iter()
.filter(|backend| selected.iter().any(|value| value == backend.as_str()))
.collect();
InstanceConfigUpdateRequest {
gateway_rollout: None,
registration: None,
sso: None,
app_public: None,
policy: None,
integrations: None,
media: None,
voice_noise_suppression: Some(VoiceNoiseSuppressionConfigUpdateRequest {
enabled: Some(form.bool_value("voice_ns_enabled")),
default_backend: form
.first("voice_ns_default_backend")
.and_then(NoiseSuppressionBackend::from_value),
enabled_backends: Some(enabled_backends),
allow_user_override: Some(form.bool_value("voice_ns_allow_user_override")),
rollout_basis_points: parse_clamped_form_number(
form,
"voice_ns_rollout_basis_points",
0,
0,
VOICE_NS_ROLLOUT_BASIS_POINTS_MAX,
),
rollout_salt: parse_voice_noise_suppression_rollout_salt(form),
included_user_ids: Some(parse_voice_noise_suppression_user_ids(
form.first("voice_ns_included_user_ids").unwrap_or_default(),
)),
excluded_user_ids: Some(parse_voice_noise_suppression_user_ids(
form.first("voice_ns_excluded_user_ids").unwrap_or_default(),
)),
guild_overrides: Some(parse_voice_noise_suppression_guild_overrides(
form.first("voice_ns_guild_overrides").unwrap_or_default(),
)),
stereo_enabled: Some(form.bool_value("voice_ns_stereo_enabled")),
suppression_strength: parse_clamped_form_number(
form,
"voice_ns_suppression_strength",
0,
0,
VOICE_NS_SUPPRESSION_STRENGTH_MAX,
),
}),
experiment_delivery: None,
}
}
fn build_experiment_delivery_update(form: &MultiValueForm) -> InstanceConfigUpdateRequest {
InstanceConfigUpdateRequest {
gateway_rollout: None,
registration: None,
sso: None,
app_public: None,
policy: None,
integrations: None,
media: None,
voice_noise_suppression: None,
experiment_delivery: Some(ExperimentDeliveryConfigUpdateRequest {
poll_interval_seconds: parse_clamped_form_number(
form,
"experiment_delivery_poll_interval_seconds",
EXPERIMENT_DEFAULT_POLL_INTERVAL_SECONDS,
EXPERIMENT_MIN_POLL_INTERVAL_SECONDS,
EXPERIMENT_MAX_POLL_INTERVAL_SECONDS,
),
poll_jitter_percent: parse_clamped_form_number(
form,
"experiment_delivery_poll_jitter_percent",
EXPERIMENT_DEFAULT_POLL_JITTER_PERCENT,
0,
EXPERIMENT_MAX_POLL_JITTER_PERCENT,
),
}),
}
}
@@ -466,6 +656,8 @@ fn build_registration_update(form: &MultiValueForm) -> InstanceConfigUpdateReque
policy: None,
integrations: None,
media: None,
voice_noise_suppression: None,
experiment_delivery: None,
}
}
@@ -494,6 +686,8 @@ fn build_app_public_update(form: &MultiValueForm) -> InstanceConfigUpdateRequest
policy: None,
integrations: None,
media: None,
voice_noise_suppression: None,
experiment_delivery: None,
}
}
@@ -515,6 +709,8 @@ fn build_app_legal_update(form: &MultiValueForm) -> InstanceConfigUpdateRequest
policy: None,
integrations: None,
media: None,
voice_noise_suppression: None,
experiment_delivery: None,
}
}
@@ -534,6 +730,8 @@ fn build_app_registration_update(form: &MultiValueForm) -> InstanceConfigUpdateR
policy: None,
integrations: None,
media: None,
voice_noise_suppression: None,
experiment_delivery: None,
}
}
@@ -563,6 +761,8 @@ fn build_policy_update(form: &MultiValueForm) -> InstanceConfigUpdateRequest {
}),
integrations: None,
media: None,
voice_noise_suppression: None,
experiment_delivery: None,
}
}
@@ -672,6 +872,8 @@ fn build_integrations_update(form: &MultiValueForm) -> InstanceConfigUpdateReque
}),
}),
media: None,
voice_noise_suppression: None,
experiment_delivery: None,
}
}
@@ -700,6 +902,8 @@ fn build_media_update(form: &MultiValueForm) -> InstanceConfigUpdateRequest {
renew_window_days: form.parse_u32("media_attachment_decay_renew_window_days"),
}),
}),
voice_noise_suppression: None,
experiment_delivery: None,
}
}
@@ -742,6 +946,8 @@ fn build_single_community_update(enabled: bool) -> InstanceConfigUpdateRequest {
}),
integrations: None,
media: None,
voice_noise_suppression: None,
experiment_delivery: None,
}
}
@@ -1066,6 +1272,326 @@ mod tests {
);
}
#[test]
fn build_voice_noise_suppression_update_collects_backends_and_clamps_numbers() {
let form = MultiValueForm::parse(
b"voice_ns_enabled=true&voice_ns_allow_user_override=on&voice_ns_default_backend=rnnoise&voice_ns_enabled_backends%5B%5D=deep_filter&voice_ns_enabled_backends%5B%5D=none&voice_ns_enabled_backends%5B%5D=bogus&voice_ns_rollout_basis_points=99999&voice_ns_suppression_strength=250&voice_ns_rollout_salt=%20voice-ns-v2%20",
);
let request = build_voice_noise_suppression_update(&form);
let update = request
.voice_noise_suppression
.expect("voice noise suppression update");
assert_eq!(update.enabled, Some(true));
assert_eq!(update.allow_user_override, Some(true));
assert_eq!(update.stereo_enabled, Some(false));
assert_eq!(
update.default_backend,
Some(NoiseSuppressionBackend::Rnnoise)
);
assert_eq!(
update.enabled_backends,
Some(vec![
NoiseSuppressionBackend::None,
NoiseSuppressionBackend::DeepFilter
])
);
assert_eq!(update.rollout_basis_points, Some(10_000));
assert_eq!(update.suppression_strength, Some(100));
assert_eq!(update.rollout_salt, Some("voice-ns-v2".to_owned()));
}
#[test]
fn build_voice_noise_suppression_update_leaves_the_feature_inert_when_nothing_is_submitted() {
let form = MultiValueForm::parse(b"_csrf=token");
let request = build_voice_noise_suppression_update(&form);
let update = request
.voice_noise_suppression
.expect("voice noise suppression update");
assert_eq!(update.enabled, Some(false));
assert_eq!(update.allow_user_override, Some(false));
assert_eq!(update.stereo_enabled, Some(false));
assert_eq!(update.default_backend, None);
assert_eq!(update.enabled_backends, Some(Vec::new()));
assert_eq!(update.rollout_basis_points, None);
assert_eq!(update.rollout_salt, None);
assert_eq!(update.included_user_ids, Some(Vec::new()));
assert_eq!(update.excluded_user_ids, Some(Vec::new()));
assert_eq!(update.guild_overrides, Some(Vec::new()));
assert_eq!(update.suppression_strength, None);
assert!(request.gateway_rollout.is_none());
assert!(request.policy.is_none());
assert!(request.media.is_none());
assert!(request.experiment_delivery.is_none());
}
#[test]
fn build_voice_noise_suppression_update_reads_user_id_textareas() {
let form = MultiValueForm::parse(
b"voice_ns_included_user_ids=1500000000000000001%0A1500000000000000002&voice_ns_excluded_user_ids=1500000000000000003%2C%201500000000000000004",
);
let update = build_voice_noise_suppression_update(&form)
.voice_noise_suppression
.expect("voice noise suppression update");
assert_eq!(
update.included_user_ids,
Some(vec![
"1500000000000000001".to_owned(),
"1500000000000000002".to_owned()
])
);
assert_eq!(
update.excluded_user_ids,
Some(vec![
"1500000000000000003".to_owned(),
"1500000000000000004".to_owned()
])
);
}
#[test]
fn parse_voice_noise_suppression_user_ids_splits_newlines_and_commas() {
assert_eq!(
parse_voice_noise_suppression_user_ids(" 1 ,2\n3\r\n 4 ,, 5 "),
vec![
"1".to_owned(),
"2".to_owned(),
"3".to_owned(),
"4".to_owned(),
"5".to_owned()
]
);
}
#[test]
fn parse_voice_noise_suppression_user_ids_dedupes_preserving_order() {
assert_eq!(
parse_voice_noise_suppression_user_ids("20,10,20,10,30"),
vec!["20".to_owned(), "10".to_owned(), "30".to_owned()]
);
}
#[test]
fn parse_voice_noise_suppression_user_ids_rejects_non_digit_and_overlong_values() {
assert!(
parse_voice_noise_suppression_user_ids(
"abc,12a,-1,1.0, ,999999999999999999999,<script>"
)
.is_empty()
);
}
#[test]
fn parse_voice_noise_suppression_user_ids_truncates_at_cap() {
let value = (0..1_200)
.map(|index| index.to_string())
.collect::<Vec<_>>()
.join("\n");
let ids = parse_voice_noise_suppression_user_ids(&value);
assert_eq!(ids.len(), VOICE_NS_MAX_TARGETED_USERS);
assert_eq!(ids.last(), Some(&"999".to_owned()));
}
#[test]
fn parse_voice_noise_suppression_guild_overrides_skips_malformed_lines() {
let overrides = parse_voice_noise_suppression_guild_overrides(
" 1600000000000000001 = rnnoise \n1600000000000000002\n=gate\nnot-a-guild=gate\n1600000000000000003=unknown_backend\n1600000000000000004=deep_filter\n",
);
assert_eq!(
overrides,
vec![
VoiceNoiseSuppressionGuildOverride {
guild_id: "1600000000000000001".to_owned(),
backend: NoiseSuppressionBackend::Rnnoise,
},
VoiceNoiseSuppressionGuildOverride {
guild_id: "1600000000000000004".to_owned(),
backend: NoiseSuppressionBackend::DeepFilter,
},
]
);
}
#[test]
fn build_voice_noise_suppression_update_treats_blank_numbers_as_explicit_values() {
let form = MultiValueForm::parse(
b"voice_ns_rollout_basis_points=&voice_ns_suppression_strength=%20%20",
);
let update = build_voice_noise_suppression_update(&form)
.voice_noise_suppression
.expect("voice noise suppression update");
assert_eq!(update.rollout_basis_points, Some(0));
assert_eq!(update.suppression_strength, Some(0));
}
#[test]
fn build_voice_noise_suppression_update_treats_unparseable_numbers_as_explicit_values() {
let form = MultiValueForm::parse(
b"voice_ns_rollout_basis_points=abc&voice_ns_suppression_strength=-5",
);
let update = build_voice_noise_suppression_update(&form)
.voice_noise_suppression
.expect("voice noise suppression update");
assert_eq!(update.rollout_basis_points, Some(0));
assert_eq!(update.suppression_strength, Some(0));
}
#[test]
fn build_voice_noise_suppression_update_accepts_padded_numbers() {
let form = MultiValueForm::parse(b"voice_ns_rollout_basis_points=%20250%20");
let update = build_voice_noise_suppression_update(&form)
.voice_noise_suppression
.expect("voice noise suppression update");
assert_eq!(update.rollout_basis_points, Some(250));
}
#[test]
fn build_voice_noise_suppression_update_truncates_an_overlong_rollout_salt() {
let salt = "\u{e9}".repeat(80);
let form = MultiValueForm::parse(format!("voice_ns_rollout_salt={salt}").as_bytes());
let update = build_voice_noise_suppression_update(&form)
.voice_noise_suppression
.expect("voice noise suppression update");
let salt = update.rollout_salt.expect("rollout salt");
assert_eq!(salt.chars().count(), VOICE_NS_MAX_ROLLOUT_SALT_CHARS);
assert_eq!(salt, "\u{e9}".repeat(VOICE_NS_MAX_ROLLOUT_SALT_CHARS));
}
#[test]
fn build_voice_noise_suppression_update_never_sends_an_empty_rollout_salt() {
let form = MultiValueForm::parse(b"voice_ns_rollout_salt=%20%20%20");
let update = build_voice_noise_suppression_update(&form)
.voice_noise_suppression
.expect("voice noise suppression update");
assert_eq!(update.rollout_salt, None);
}
#[test]
fn parse_voice_noise_suppression_guild_overrides_dedupes_keeping_the_first_backend() {
let overrides = parse_voice_noise_suppression_guild_overrides(
"1600000000000000001=rnnoise\n1600000000000000001=gate\n1600000000000000002=speex\n1600000000000000001=deep_filter\n",
);
assert_eq!(
overrides,
vec![
VoiceNoiseSuppressionGuildOverride {
guild_id: "1600000000000000001".to_owned(),
backend: NoiseSuppressionBackend::Rnnoise,
},
VoiceNoiseSuppressionGuildOverride {
guild_id: "1600000000000000002".to_owned(),
backend: NoiseSuppressionBackend::Speex,
},
]
);
}
#[test]
fn parse_voice_noise_suppression_guild_overrides_truncates_at_cap() {
let value = (0..300)
.map(|index| format!("{index}=gate"))
.collect::<Vec<_>>()
.join("\n");
let overrides = parse_voice_noise_suppression_guild_overrides(&value);
assert_eq!(overrides.len(), VOICE_NS_MAX_GUILD_OVERRIDES);
assert_eq!(
overrides.last().map(|entry| entry.guild_id.as_str()),
Some("199")
);
}
#[test]
fn build_experiment_delivery_update_leaves_both_fields_unchanged_when_absent() {
let form = MultiValueForm::parse(b"_csrf=token");
let request = build_experiment_delivery_update(&form);
let update = request
.experiment_delivery
.expect("experiment delivery update");
assert_eq!(update.poll_interval_seconds, None);
assert_eq!(update.poll_jitter_percent, None);
assert!(request.voice_noise_suppression.is_none());
assert!(request.gateway_rollout.is_none());
assert!(request.policy.is_none());
assert!(request.media.is_none());
}
#[test]
fn build_experiment_delivery_update_treats_blank_numbers_as_explicit_defaults() {
let form = MultiValueForm::parse(
b"experiment_delivery_poll_interval_seconds=&experiment_delivery_poll_jitter_percent=%20%20",
);
let update = build_experiment_delivery_update(&form)
.experiment_delivery
.expect("experiment delivery update");
assert_eq!(
update.poll_interval_seconds,
Some(EXPERIMENT_DEFAULT_POLL_INTERVAL_SECONDS)
);
assert_eq!(
update.poll_jitter_percent,
Some(EXPERIMENT_DEFAULT_POLL_JITTER_PERCENT)
);
}
#[test]
fn build_experiment_delivery_update_treats_unparseable_numbers_as_explicit_defaults() {
let form = MultiValueForm::parse(
b"experiment_delivery_poll_interval_seconds=12.5&experiment_delivery_poll_jitter_percent=abc",
);
let update = build_experiment_delivery_update(&form)
.experiment_delivery
.expect("experiment delivery update");
assert_eq!(
update.poll_interval_seconds,
Some(EXPERIMENT_DEFAULT_POLL_INTERVAL_SECONDS)
);
assert_eq!(
update.poll_jitter_percent,
Some(EXPERIMENT_DEFAULT_POLL_JITTER_PERCENT)
);
}
#[test]
fn build_experiment_delivery_update_clamps_numbers_to_their_bounds() {
let form = MultiValueForm::parse(
b"experiment_delivery_poll_interval_seconds=5&experiment_delivery_poll_jitter_percent=90",
);
let update = build_experiment_delivery_update(&form)
.experiment_delivery
.expect("experiment delivery update");
assert_eq!(
update.poll_interval_seconds,
Some(EXPERIMENT_MIN_POLL_INTERVAL_SECONDS)
);
assert_eq!(
update.poll_jitter_percent,
Some(EXPERIMENT_MAX_POLL_JITTER_PERCENT)
);
let form = MultiValueForm::parse(
b"experiment_delivery_poll_interval_seconds=999999&experiment_delivery_poll_jitter_percent=0",
);
let update = build_experiment_delivery_update(&form)
.experiment_delivery
.expect("experiment delivery update");
assert_eq!(
update.poll_interval_seconds,
Some(EXPERIMENT_MAX_POLL_INTERVAL_SECONDS)
);
assert_eq!(update.poll_jitter_percent, Some(0));
}
#[test]
fn build_experiment_delivery_update_accepts_padded_numbers() {
let form = MultiValueForm::parse(
b"experiment_delivery_poll_interval_seconds=%20900%20&experiment_delivery_poll_jitter_percent=%2025%20",
);
let update = build_experiment_delivery_update(&form)
.experiment_delivery
.expect("experiment delivery update");
assert_eq!(update.poll_interval_seconds, Some(900));
assert_eq!(update.poll_jitter_percent, Some(25));
}
#[test]
fn update_limit_rule_values_reads_checked_limit_keys() {
let form = MultiValueForm::parse(b"message_send=1&traits%5B%5D=trial");
@@ -2,10 +2,12 @@
use crate::{
api::types::{
AppPublicConfigResponse, GatewayRolloutConfigResponse, InstanceConfigResponse,
InstanceIntegrationsResponse, InstanceMediaResponse, InstancePolicyResponse,
InstanceRegistrationResponse, LimitConfigResponse, PendingRegistrationResponse,
RegistrationUrlResponse, SsoConfigResponse,
AppPublicConfigResponse, ExperimentDeliveryConfigResponse, GatewayRolloutConfigResponse,
InstanceConfigResponse, InstanceIntegrationsResponse, InstanceMediaResponse,
InstancePolicyResponse, InstanceRegistrationResponse, LimitConfigResponse,
NoiseSuppressionBackend, PendingRegistrationResponse, RegistrationUrlResponse,
SsoConfigResponse, VOICE_NS_MAX_GUILD_OVERRIDES, VOICE_NS_MAX_TARGETED_USERS,
VoiceNoiseSuppressionConfigResponse,
},
config::AdminConfig,
middleware::auth::AuthContext,
@@ -42,6 +44,17 @@ fn format_decimal(value: f64) -> String {
}
}
fn entry_count_hint(count: usize, cap: usize) -> Markup {
html! {
p class="text-xs text-neutral-500" {
(count) " of " (cap) " stored"
@if count >= cap {
" (at the cap; extra lines are dropped on save)"
}
}
}
}
fn number_field(
name: &str,
label: &str,
@@ -134,6 +147,8 @@ pub fn instance_config_page(
"Gateway rollout behavior and the limit rules applied to users and guilds.",
html! {
(gateway_rollout_section(base, csrf_token, &instance_config.gateway_rollout))
(voice_noise_suppression_section(base, csrf_token, &instance_config.voice_noise_suppression))
(experiment_delivery_section(base, csrf_token, &instance_config.experiment_delivery))
@if let Some(limit_config) = limit_config {
(limit_config_section(base, limit_config))
} @else {
@@ -953,6 +968,249 @@ fn gateway_rollout_section(
)
}
fn voice_noise_suppression_section(
base: &str,
csrf_token: &str,
voice_noise_suppression: &VoiceNoiseSuppressionConfigResponse,
) -> Markup {
let status = if voice_noise_suppression.enabled {
("Live", BadgeVariant::Success)
} else {
("Inert", BadgeVariant::Default)
};
let backend_options = NoiseSuppressionBackend::ALL
.iter()
.map(|backend| (backend.as_str(), backend.label()))
.collect::<Vec<_>>();
let included_user_ids = voice_noise_suppression.included_user_ids.join("\n");
let excluded_user_ids = voice_noise_suppression.excluded_user_ids.join("\n");
let guild_overrides = voice_noise_suppression
.guild_overrides
.iter()
.map(|entry| format!("{}={}", entry.guild_id, entry.backend.as_str()))
.collect::<Vec<_>>()
.join("\n");
section_card_with_description(
"Voice Noise Suppression",
"Pick which noise suppression backend targeted clients load in voice calls, and how many \
of them are targeted. While the master switch below is off nothing on this form reaches \
any client: every user keeps the audio pipeline they have today, whatever the rest of \
these fields say.",
html! {
form method="post" action={(base) "/instance-config?action=update_voice_noise_suppression"} {
(csrf_input(csrf_token))
div class="space-y-6" {
div class="flex flex-wrap items-center gap-2" {
h3 class="text-sm font-semibold text-neutral-900" { "Master switch" }
(badge(status.0, status.1))
span class="text-xs text-neutral-500" {
"Config version " (voice_noise_suppression.config_version)
}
}
(checkbox(
"voice_ns_enabled",
"true",
"Serve noise suppression assignments to clients",
voice_noise_suppression.enabled,
true,
))
p class="text-xs text-neutral-500" {
"Off is the safe state. With this unchecked every client is told the \
feature is inert and keeps its current behavior, so the rollout, targeting \
and override fields below have no effect at all."
}
h3 class="text-sm font-semibold text-neutral-900" { "Backends" }
(select_input(
"voice_ns_default_backend",
"Default Backend",
&backend_options,
voice_noise_suppression.default_backend.as_str(),
))
p class="text-xs text-neutral-500" {
"The backend handed to every targeted user. A default that is not ticked \
below is treated as unavailable and nobody is targeted."
}
div class="grid grid-cols-1 gap-2 sm:grid-cols-2" {
@for backend in NoiseSuppressionBackend::ALL {
(checkbox(
"voice_ns_enabled_backends[]",
backend.as_str(),
backend.label(),
voice_noise_suppression.enabled_backends.contains(&backend),
true,
))
}
}
p class="text-xs text-neutral-500" {
"Backends clients are allowed to load. Unticking one withdraws it from \
every user, including anyone who picked it themselves."
}
(checkbox(
"voice_ns_allow_user_override",
"true",
"Let users pick their own backend from the ticked list",
voice_noise_suppression.allow_user_override,
true,
))
p class="text-xs text-neutral-500" {
"Applies only to users who are already targeted. It never pulls anyone \
into the rollout."
}
h3 class="text-sm font-semibold text-neutral-900" { "Rollout" }
(number_field(
"voice_ns_rollout_basis_points",
"Rollout (basis points)",
&voice_noise_suppression.rollout_basis_points.to_string(),
Some(0), Some(10000), "1",
Some("Share of users bucketed into the canary, in basis points: 0 is nobody, 100 is 1%, 10000 is everybody."),
))
div class="flex flex-col gap-2" {
(text_input(
"voice_ns_rollout_salt",
"Rollout Salt",
&voice_noise_suppression.rollout_salt,
"voice-ns-v1",
))
p class="text-xs text-neutral-500" {
"Seeds the bucketing hash. Changing it reshuffles which users fall \
inside the percentage above. Leave it alone to keep the current \
cohort stable."
}
}
div class="flex flex-col gap-2" {
(textarea_input(
"voice_ns_included_user_ids",
"Always-on User IDs",
"1500000000000000001\n1500000000000000002",
&included_user_ids,
4,
false,
))
(entry_count_hint(
voice_noise_suppression.included_user_ids.len(),
VOICE_NS_MAX_TARGETED_USERS,
))
p class="text-xs text-neutral-500" {
"One snowflake per line, or comma separated. These users are targeted \
regardless of the percentage above. Anything that is not a plain \
numeric ID is dropped."
}
}
div class="flex flex-col gap-2" {
(textarea_input(
"voice_ns_excluded_user_ids",
"Never-on User IDs",
"1500000000000000003\n1500000000000000004",
&excluded_user_ids,
4,
false,
))
(entry_count_hint(
voice_noise_suppression.excluded_user_ids.len(),
VOICE_NS_MAX_TARGETED_USERS,
))
p class="text-xs text-neutral-500" {
"Same format. Exclusion wins over both the always-on list and the \
percentage, so this is the per-user kill switch."
}
}
h3 class="text-sm font-semibold text-neutral-900" { "Per-guild overrides" }
div class="flex flex-col gap-2" {
(textarea_input(
"voice_ns_guild_overrides",
"Guild Overrides",
"1600000000000000001=rnnoise\n1600000000000000002=deep_filter",
&guild_overrides,
4,
false,
))
(entry_count_hint(
voice_noise_suppression.guild_overrides.len(),
VOICE_NS_MAX_GUILD_OVERRIDES,
))
p class="text-xs text-neutral-500" {
"One per line as guild_id=backend, first line per guild wins. Targeted \
users in that guild get that backend instead of the default. Lines \
that are not a numeric guild ID and a ticked backend are dropped \
without failing the save."
}
}
h3 class="text-sm font-semibold text-neutral-900" { "Processing" }
(checkbox(
"voice_ns_stereo_enabled",
"true",
"Process stereo input instead of downmixing to mono",
voice_noise_suppression.stereo_enabled,
true,
))
p class="text-xs text-neutral-500" {
"Costs more CPU on the client. Leave off unless you are testing stereo \
capture."
}
div class="grid grid-cols-1 gap-4 sm:grid-cols-2" {
(number_field(
"voice_ns_suppression_strength",
"Suppression Strength",
&voice_noise_suppression.suppression_strength.to_string(),
Some(0), Some(100), "1",
Some("How aggressively the backend removes noise, 0 to 100. Higher values cut more background but chew more of the voice."),
))
}
(form_actions(html! {
(submit_button("Save Voice Noise Suppression Configuration"))
}))
}
}
},
)
}
fn experiment_delivery_section(
base: &str,
csrf_token: &str,
experiment_delivery: &ExperimentDeliveryConfigResponse,
) -> Markup {
section_card_with_description(
"Experiment Delivery",
"How often every client revalidates its experiment assignments. This is instance-wide \
and covers every experiment, not just the one above. Raising the interval sheds \
request volume and makes a change take longer to reach a client. Raising the jitter \
spreads a fleet that has synchronised on one tick back out across the interval.",
html! {
form method="post" action={(base) "/instance-config?action=update_experiment_delivery"} {
(csrf_input(csrf_token))
div class="space-y-6" {
div class="grid grid-cols-1 gap-4 sm:grid-cols-2" {
(number_field(
"experiment_delivery_poll_interval_seconds",
"Assignment Poll Interval (s)",
&experiment_delivery.poll_interval_seconds.to_string(),
Some(60), Some(86400), "1",
Some("How often a client re-reads its assignments, 60 to 86400 seconds. Lower values pick up changes sooner at the cost of more requests."),
))
(number_field(
"experiment_delivery_poll_jitter_percent",
"Assignment Poll Jitter (%)",
&experiment_delivery.poll_jitter_percent.to_string(),
Some(0), Some(50), "1",
Some("How far each client spreads its poll around the interval, 0 to 50 percent. Raise it to break up a fleet that polls on the same tick, set it to 0 for an exact interval."),
))
}
(form_actions(html! {
(submit_button("Save Experiment Delivery Configuration"))
}))
}
}
},
)
}
fn registration_config_section(
config: &AdminConfig,
csrf_token: &str,
@@ -1522,3 +1780,49 @@ fn limit_config_section(base: &str, limit_config: &LimitConfigResponse) -> Marku
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::types::VoiceNoiseSuppressionGuildOverride;
fn rendered_voice_noise_suppression_section(
voice_noise_suppression: &VoiceNoiseSuppressionConfigResponse,
) -> String {
voice_noise_suppression_section("/admin", "csrf", voice_noise_suppression).into_string()
}
#[test]
fn voice_noise_suppression_section_shows_list_counts_and_caps() {
let voice_noise_suppression = VoiceNoiseSuppressionConfigResponse {
included_user_ids: vec!["1500000000000000001".to_owned()],
excluded_user_ids: vec![
"1500000000000000002".to_owned(),
"1500000000000000003".to_owned(),
],
guild_overrides: vec![VoiceNoiseSuppressionGuildOverride {
guild_id: "1600000000000000001".to_owned(),
backend: NoiseSuppressionBackend::Rnnoise,
}],
..VoiceNoiseSuppressionConfigResponse::default()
};
let markup = rendered_voice_noise_suppression_section(&voice_noise_suppression);
assert!(markup.contains("1 of 1000 stored"));
assert!(markup.contains("2 of 1000 stored"));
assert!(markup.contains("1 of 200 stored"));
assert!(!markup.contains("at the cap"));
}
#[test]
fn voice_noise_suppression_section_flags_a_list_at_its_cap() {
let voice_noise_suppression = VoiceNoiseSuppressionConfigResponse {
included_user_ids: (0..VOICE_NS_MAX_TARGETED_USERS)
.map(|index| index.to_string())
.collect(),
..VoiceNoiseSuppressionConfigResponse::default()
};
let markup = rendered_voice_noise_suppression_section(&voice_noise_suppression);
assert!(markup.contains("1000 of 1000 stored"));
assert!(markup.contains("at the cap"));
}
}