mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
feat(voice): add noise suppression backends and rollout (#2706)
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user