mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
feat(voice): ship noise suppression treatment to everyone (#3029)
This commit is contained in:
@@ -32,10 +32,6 @@ import {
|
||||
ProfileTimezoneConfigUpdateRequest,
|
||||
} from '@fluxer/schema/src/domains/admin/ProfileTimezoneSchemas';
|
||||
import {PushRelayConfigResponse, PushRelayConfigUpdateRequest} from '@fluxer/schema/src/domains/admin/PushRelaySchemas';
|
||||
import {
|
||||
VoiceNoiseSuppressionConfigResponse,
|
||||
VoiceNoiseSuppressionConfigUpdateRequest,
|
||||
} from '@fluxer/schema/src/domains/admin/VoiceNoiseSuppressionSchemas';
|
||||
import {
|
||||
ExperimentDeliveryConfigResponse,
|
||||
ExperimentDeliveryConfigUpdateRequest,
|
||||
@@ -675,7 +671,6 @@ const InstanceIntegrationsResponse = z.object({
|
||||
export const InstanceConfigResponse = z.object({
|
||||
sso: SsoConfigResponse,
|
||||
gateway_rollout: GatewayRolloutConfigResponse,
|
||||
voice_noise_suppression: VoiceNoiseSuppressionConfigResponse,
|
||||
push_relay: PushRelayConfigResponse,
|
||||
domain_migration: DomainMigrationConfigResponse,
|
||||
altcha_captcha: AltchaCaptchaConfigResponse,
|
||||
@@ -716,7 +711,6 @@ const InstancePolicyUpdateSchema = z.object({
|
||||
|
||||
export const InstanceConfigUpdateRequest = z.object({
|
||||
gateway_rollout: GatewayRolloutConfigUpdateRequest.nullish(),
|
||||
voice_noise_suppression: VoiceNoiseSuppressionConfigUpdateRequest.nullish(),
|
||||
push_relay: PushRelayConfigUpdateRequest.nullish(),
|
||||
domain_migration: DomainMigrationConfigUpdateRequest.nullish(),
|
||||
altcha_captcha: AltchaCaptchaConfigUpdateRequest.nullish(),
|
||||
|
||||
@@ -1,459 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG,
|
||||
INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT,
|
||||
resolveVoiceNoiseSuppressionAssignment,
|
||||
resolveVoiceNoiseSuppressionForCall,
|
||||
VOICE_NOISE_SUPPRESSION_BACKENDS,
|
||||
type VoiceNoiseSuppressionAssignmentResponse,
|
||||
type VoiceNoiseSuppressionConfig,
|
||||
VoiceNoiseSuppressionConfigSchema,
|
||||
VoiceNoiseSuppressionConfigUpdateRequest,
|
||||
} from '@fluxer/schema/src/domains/admin/VoiceNoiseSuppressionSchemas';
|
||||
import {type ExperimentTargeting, experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
const NO_TARGETING: ExperimentTargeting = {memberGuildIds: new Set(), premium: false};
|
||||
|
||||
const TARGETED_USER_ID = '1000000000000000001';
|
||||
const OTHER_USER_ID = '1000000000000000002';
|
||||
const GUILD_ID = '2000000000000000001';
|
||||
const OTHER_GUILD_ID = '2000000000000000002';
|
||||
|
||||
describe('voice noise suppression configuration', () => {
|
||||
test('derives defaults from the schema with independently owned arrays', () => {
|
||||
const first = VoiceNoiseSuppressionConfigSchema.parse({});
|
||||
const second = VoiceNoiseSuppressionConfigSchema.parse({});
|
||||
expect(first).toEqual(DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG);
|
||||
first.enabled_backends.pop();
|
||||
first.included_user_ids.push(TARGETED_USER_ID);
|
||||
first.excluded_user_ids.push(OTHER_USER_ID);
|
||||
first.guild_overrides.push({guild_id: GUILD_ID, backend: 'rnnoise'});
|
||||
expect(second).toEqual(DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG);
|
||||
});
|
||||
|
||||
test.each([{}, {enabled: false}, {enabled: undefined}, {suppression_strength: 42}])(
|
||||
'keeps partial updates free of configuration defaults: %j',
|
||||
(patch) => {
|
||||
expect(VoiceNoiseSuppressionConfigUpdateRequest.parse(patch)).toEqual(patch);
|
||||
},
|
||||
);
|
||||
|
||||
test('does not accept a client-provided configuration version', () => {
|
||||
expect(VoiceNoiseSuppressionConfigUpdateRequest.parse({config_version: 12})).toEqual({});
|
||||
});
|
||||
|
||||
test.each([
|
||||
{rollout_basis_points: -1},
|
||||
{rollout_basis_points: 10001},
|
||||
{suppression_strength: -1},
|
||||
{suppression_strength: 101},
|
||||
{rollout_salt: ' '},
|
||||
{included_user_ids: ['not-an-id']},
|
||||
{guild_overrides: [{guild_id: GUILD_ID, backend: 'unknown'}]},
|
||||
])('applies the same validation to stored configuration and updates: %j', (value) => {
|
||||
expect(VoiceNoiseSuppressionConfigSchema.safeParse(value).success).toBe(false);
|
||||
expect(VoiceNoiseSuppressionConfigUpdateRequest.safeParse(value).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
function createConfig(overrides: Partial<VoiceNoiseSuppressionConfig> = {}): VoiceNoiseSuppressionConfig {
|
||||
return {
|
||||
...DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG,
|
||||
enabled_backends: [...DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG.enabled_backends],
|
||||
included_user_ids: [],
|
||||
excluded_user_ids: [],
|
||||
guild_overrides: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createAssignment(
|
||||
overrides: Partial<VoiceNoiseSuppressionAssignmentResponse> = {},
|
||||
): VoiceNoiseSuppressionAssignmentResponse {
|
||||
return {
|
||||
enabled: true,
|
||||
config_version: 7,
|
||||
user_targeted: true,
|
||||
backend: 'rnnoise',
|
||||
source: 'canary',
|
||||
guild_overrides: [],
|
||||
enabled_backends: [...VOICE_NOISE_SUPPRESSION_BACKENDS],
|
||||
allow_user_override: false,
|
||||
suppression_strength: 80,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function syntheticUserIds(count: number): Array<string> {
|
||||
const ids: Array<string> = [];
|
||||
for (let index = 0; index < count; index++) {
|
||||
ids.push((1400000000000000000n + BigInt(index)).toString());
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function targetedUserIds(config: VoiceNoiseSuppressionConfig, userIds: ReadonlyArray<string>): Set<string> {
|
||||
const targeted = new Set<string>();
|
||||
for (const userId of userIds) {
|
||||
if (resolveVoiceNoiseSuppressionAssignment(config, userId, NO_TARGETING).user_targeted) {
|
||||
targeted.add(userId);
|
||||
}
|
||||
}
|
||||
return targeted;
|
||||
}
|
||||
|
||||
describe('experimentBucket', () => {
|
||||
test('stays inside the basis point range for every synthetic id', () => {
|
||||
for (const userId of syntheticUserIds(500)) {
|
||||
const bucket = experimentBucket(userId, 'voice-ns-v1');
|
||||
expect(Number.isInteger(bucket)).toBe(true);
|
||||
expect(bucket).toBeGreaterThanOrEqual(0);
|
||||
expect(bucket).toBeLessThan(10000);
|
||||
}
|
||||
});
|
||||
|
||||
test.each([
|
||||
{userId: TARGETED_USER_ID, salt: 'voice-ns-v1', expected: 8241},
|
||||
{userId: TARGETED_USER_ID, salt: 'voice-ns-v2', expected: 8500},
|
||||
{userId: OTHER_USER_ID, salt: 'voice-ns-v1', expected: 5384},
|
||||
{userId: OTHER_USER_ID, salt: 'voice-ns-v2', expected: 1357},
|
||||
])('preserves the assignment bucket for $userId with salt $salt', ({userId, salt, expected}) => {
|
||||
expect(experimentBucket(userId, salt)).toBe(expected);
|
||||
});
|
||||
|
||||
test('changes with the salt for at least most user ids', () => {
|
||||
const userIds = syntheticUserIds(200);
|
||||
const changed = userIds.filter(
|
||||
(userId) => experimentBucket(userId, 'voice-ns-v1') !== experimentBucket(userId, 'voice-ns-v2'),
|
||||
);
|
||||
expect(changed.length).toBeGreaterThan(userIds.length - 5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveVoiceNoiseSuppressionAssignment', () => {
|
||||
test('returns the inert assignment when the master switch is off', () => {
|
||||
const config = createConfig({
|
||||
enabled: false,
|
||||
config_version: 4,
|
||||
rollout_basis_points: 10000,
|
||||
included_user_ids: [TARGETED_USER_ID],
|
||||
guild_overrides: [{guild_id: GUILD_ID, backend: 'rnnoise'}],
|
||||
});
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual({
|
||||
...INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT,
|
||||
config_version: 4,
|
||||
});
|
||||
});
|
||||
|
||||
test('returns the inert assignment for the default config', () => {
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(createConfig(), TARGETED_USER_ID, NO_TARGETING)).toEqual(
|
||||
INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT,
|
||||
);
|
||||
});
|
||||
|
||||
test('denylist beats allowlist', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
included_user_ids: [TARGETED_USER_ID],
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, NO_TARGETING);
|
||||
expect(assignment.user_targeted).toBe(false);
|
||||
expect(assignment.backend).toBeNull();
|
||||
expect(assignment.source).toBeNull();
|
||||
});
|
||||
|
||||
test('denylist beats canary', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
rollout_basis_points: 10000,
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, NO_TARGETING);
|
||||
expect(assignment.user_targeted).toBe(false);
|
||||
expect(assignment.source).toBeNull();
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(config, OTHER_USER_ID, NO_TARGETING).user_targeted).toBe(true);
|
||||
});
|
||||
|
||||
test('denylist strips guild overrides and the client-side knobs', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
allow_user_override: true,
|
||||
rollout_basis_points: 10000,
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
guild_overrides: [{guild_id: GUILD_ID, backend: 'rnnoise'}],
|
||||
});
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, NO_TARGETING);
|
||||
expect(assignment.guild_overrides).toEqual([]);
|
||||
expect(assignment.enabled_backends).toEqual([]);
|
||||
expect(assignment.allow_user_override).toBe(false);
|
||||
});
|
||||
|
||||
test('allowlist gives source user_rule outside the canary', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
default_backend: 'gtcrn',
|
||||
rollout_basis_points: 0,
|
||||
included_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, NO_TARGETING);
|
||||
expect(assignment.user_targeted).toBe(true);
|
||||
expect(assignment.backend).toBe('gtcrn');
|
||||
expect(assignment.source).toBe('user_rule');
|
||||
});
|
||||
|
||||
test.each([
|
||||
{basisPoints: 0, targeted: false},
|
||||
{basisPoints: 10000, targeted: true},
|
||||
])('canary at $basisPoints basis points targets $targeted', ({basisPoints, targeted}) => {
|
||||
const config = createConfig({enabled: true, rollout_basis_points: basisPoints});
|
||||
for (const userId of syntheticUserIds(200)) {
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, userId, NO_TARGETING);
|
||||
expect(assignment.user_targeted).toBe(targeted);
|
||||
expect(assignment.source).toBe(targeted ? 'canary' : null);
|
||||
}
|
||||
});
|
||||
|
||||
test('raising the rollout basis points only ever adds users', () => {
|
||||
const userIds = syntheticUserIds(2000);
|
||||
const atOneThousand = targetedUserIds(createConfig({enabled: true, rollout_basis_points: 1000}), userIds);
|
||||
const atTwoThousand = targetedUserIds(createConfig({enabled: true, rollout_basis_points: 2000}), userIds);
|
||||
expect(atOneThousand.size).toBeGreaterThan(0);
|
||||
for (const userId of atOneThousand) {
|
||||
expect(atTwoThousand.has(userId)).toBe(true);
|
||||
}
|
||||
expect(atTwoThousand.size).toBeGreaterThan(atOneThousand.size);
|
||||
});
|
||||
|
||||
test('the canary set follows the salt', () => {
|
||||
const userIds = syntheticUserIds(2000);
|
||||
const first = targetedUserIds(
|
||||
createConfig({enabled: true, rollout_basis_points: 5000, rollout_salt: 'voice-ns-v1'}),
|
||||
userIds,
|
||||
);
|
||||
const second = targetedUserIds(
|
||||
createConfig({enabled: true, rollout_basis_points: 5000, rollout_salt: 'voice-ns-v2'}),
|
||||
userIds,
|
||||
);
|
||||
expect(first).not.toEqual(second);
|
||||
});
|
||||
|
||||
test('a default backend missing from enabled_backends yields no backend and no source', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
default_backend: 'deep_filter',
|
||||
enabled_backends: ['none', 'standard'],
|
||||
rollout_basis_points: 10000,
|
||||
included_user_ids: [OTHER_USER_ID],
|
||||
});
|
||||
for (const userId of [TARGETED_USER_ID, OTHER_USER_ID]) {
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, userId, NO_TARGETING);
|
||||
expect(assignment.user_targeted).toBe(false);
|
||||
expect(assignment.backend).toBeNull();
|
||||
expect(assignment.source).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
test('guild overrides naming a disabled backend are filtered out', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
default_backend: 'standard',
|
||||
enabled_backends: ['none', 'standard', 'rnnoise'],
|
||||
rollout_basis_points: 10000,
|
||||
guild_overrides: [
|
||||
{guild_id: GUILD_ID, backend: 'rnnoise'},
|
||||
{guild_id: OTHER_GUILD_ID, backend: 'deep_filter'},
|
||||
],
|
||||
});
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, NO_TARGETING).guild_overrides).toEqual([
|
||||
{guild_id: GUILD_ID, backend: 'rnnoise'},
|
||||
]);
|
||||
});
|
||||
|
||||
test('guild overrides survive the allowlist path too', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
enabled_backends: ['none', 'standard', 'rnnoise'],
|
||||
included_user_ids: [TARGETED_USER_ID],
|
||||
guild_overrides: [
|
||||
{guild_id: GUILD_ID, backend: 'rnnoise'},
|
||||
{guild_id: OTHER_GUILD_ID, backend: 'gtcrn'},
|
||||
],
|
||||
});
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, NO_TARGETING).guild_overrides).toEqual([
|
||||
{guild_id: GUILD_ID, backend: 'rnnoise'},
|
||||
]);
|
||||
});
|
||||
|
||||
test('echoes the config version on every path', () => {
|
||||
const config = createConfig({enabled: true, config_version: 11, rollout_basis_points: 10000});
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, NO_TARGETING).config_version).toBe(11);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveVoiceNoiseSuppressionForCall', () => {
|
||||
test('returns null when the master switch is off', () => {
|
||||
const assignment = createAssignment({
|
||||
enabled: false,
|
||||
user_targeted: true,
|
||||
backend: 'rnnoise',
|
||||
source: 'user_rule',
|
||||
allow_user_override: true,
|
||||
guild_overrides: [{guild_id: GUILD_ID, backend: 'gtcrn'}],
|
||||
});
|
||||
expect(resolveVoiceNoiseSuppressionForCall(assignment, GUILD_ID, 'speex')).toBeNull();
|
||||
expect(resolveVoiceNoiseSuppressionForCall(assignment, null, null)).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null when nothing targets the user', () => {
|
||||
const assignment = createAssignment({user_targeted: false, backend: null, source: null});
|
||||
expect(resolveVoiceNoiseSuppressionForCall(assignment, GUILD_ID, null)).toBeNull();
|
||||
expect(resolveVoiceNoiseSuppressionForCall(assignment, null, null)).toBeNull();
|
||||
});
|
||||
|
||||
test('user_rule beats guild_rule', () => {
|
||||
const assignment = createAssignment({
|
||||
backend: 'rnnoise',
|
||||
source: 'user_rule',
|
||||
guild_overrides: [{guild_id: GUILD_ID, backend: 'gtcrn'}],
|
||||
});
|
||||
expect(resolveVoiceNoiseSuppressionForCall(assignment, GUILD_ID, null)).toMatchObject({
|
||||
backend: 'rnnoise',
|
||||
source: 'user_rule',
|
||||
});
|
||||
});
|
||||
|
||||
test('guild_rule beats canary', () => {
|
||||
const assignment = createAssignment({
|
||||
backend: 'rnnoise',
|
||||
source: 'canary',
|
||||
guild_overrides: [{guild_id: GUILD_ID, backend: 'gtcrn'}],
|
||||
});
|
||||
expect(resolveVoiceNoiseSuppressionForCall(assignment, GUILD_ID, null)).toMatchObject({
|
||||
backend: 'gtcrn',
|
||||
source: 'guild_rule',
|
||||
});
|
||||
});
|
||||
|
||||
test('a guild override targets an otherwise untargeted user', () => {
|
||||
const assignment = createAssignment({
|
||||
user_targeted: false,
|
||||
backend: null,
|
||||
source: null,
|
||||
guild_overrides: [{guild_id: GUILD_ID, backend: 'speex'}],
|
||||
});
|
||||
expect(resolveVoiceNoiseSuppressionForCall(assignment, GUILD_ID, null)).toMatchObject({
|
||||
backend: 'speex',
|
||||
source: 'guild_rule',
|
||||
});
|
||||
expect(resolveVoiceNoiseSuppressionForCall(assignment, OTHER_GUILD_ID, null)).toBeNull();
|
||||
});
|
||||
|
||||
test('a DM falls through to the user level result', () => {
|
||||
const assignment = createAssignment({
|
||||
backend: 'rnnoise',
|
||||
source: 'canary',
|
||||
guild_overrides: [{guild_id: GUILD_ID, backend: 'gtcrn'}],
|
||||
});
|
||||
expect(resolveVoiceNoiseSuppressionForCall(assignment, null, null)).toMatchObject({
|
||||
backend: 'rnnoise',
|
||||
source: 'canary',
|
||||
});
|
||||
});
|
||||
|
||||
test.each([
|
||||
{allowUserOverride: true, preference: 'speex', expectedBackend: 'speex', expectedSource: 'user_override'},
|
||||
{allowUserOverride: true, preference: 'gtcrn', expectedBackend: 'rnnoise', expectedSource: 'canary'},
|
||||
{allowUserOverride: false, preference: 'speex', expectedBackend: 'rnnoise', expectedSource: 'canary'},
|
||||
{allowUserOverride: true, preference: null, expectedBackend: 'rnnoise', expectedSource: 'canary'},
|
||||
] as const)(
|
||||
'allow_user_override $allowUserOverride with preference $preference resolves to $expectedSource',
|
||||
({allowUserOverride, preference, expectedBackend, expectedSource}) => {
|
||||
const assignment = createAssignment({
|
||||
backend: 'rnnoise',
|
||||
source: 'canary',
|
||||
allow_user_override: allowUserOverride,
|
||||
enabled_backends: ['none', 'standard', 'speex', 'rnnoise'],
|
||||
});
|
||||
expect(resolveVoiceNoiseSuppressionForCall(assignment, null, preference)).toMatchObject({
|
||||
backend: expectedBackend,
|
||||
source: expectedSource,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test('a user override also outranks a guild rule', () => {
|
||||
const assignment = createAssignment({
|
||||
user_targeted: false,
|
||||
backend: null,
|
||||
source: null,
|
||||
allow_user_override: true,
|
||||
enabled_backends: ['none', 'standard', 'speex'],
|
||||
guild_overrides: [{guild_id: GUILD_ID, backend: 'standard'}],
|
||||
});
|
||||
expect(resolveVoiceNoiseSuppressionForCall(assignment, GUILD_ID, 'speex')).toMatchObject({
|
||||
backend: 'speex',
|
||||
source: 'user_override',
|
||||
});
|
||||
});
|
||||
|
||||
test('carries the client knobs through', () => {
|
||||
const assignment = createAssignment({
|
||||
backend: 'rnnoise',
|
||||
source: 'user_rule',
|
||||
suppression_strength: 42,
|
||||
config_version: 19,
|
||||
});
|
||||
expect(resolveVoiceNoiseSuppressionForCall(assignment, null, null)).toEqual({
|
||||
backend: 'rnnoise',
|
||||
source: 'user_rule',
|
||||
suppressionStrength: 42,
|
||||
configVersion: 19,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveVoiceNoiseSuppressionAssignment guild targeting', () => {
|
||||
const INCLUDED_GUILD_ID = '3000000000000000001';
|
||||
const MEMBER_GUILDS: ExperimentTargeting = {
|
||||
memberGuildIds: new Set(['3000000000000000009', INCLUDED_GUILD_ID]),
|
||||
premium: false,
|
||||
};
|
||||
|
||||
test('targets members of an included guild the way it targets included users', () => {
|
||||
const config = createConfig({enabled: true, included_guild_ids: [INCLUDED_GUILD_ID]});
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, MEMBER_GUILDS);
|
||||
expect(assignment.user_targeted).toBe(true);
|
||||
expect(assignment.source).toBe('user_rule');
|
||||
expect(assignment.backend).toBe(config.default_backend);
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, NO_TARGETING).user_targeted).toBe(false);
|
||||
});
|
||||
|
||||
test('keeps user exclusions ahead of guild membership', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
included_guild_ids: [INCLUDED_GUILD_ID],
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, MEMBER_GUILDS);
|
||||
expect(assignment.user_targeted).toBe(false);
|
||||
expect(assignment.source).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveVoiceNoiseSuppressionAssignment premium targeting', () => {
|
||||
const PREMIUM: ExperimentTargeting = {memberGuildIds: new Set(), premium: true};
|
||||
|
||||
test('targets premium users only when the switch is on', () => {
|
||||
const on = createConfig({enabled: true, include_premium_users: true});
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(on, TARGETED_USER_ID, PREMIUM)).toMatchObject({
|
||||
user_targeted: true,
|
||||
source: 'user_rule',
|
||||
});
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(on, TARGETED_USER_ID, NO_TARGETING).user_targeted).toBe(false);
|
||||
const off = createConfig({enabled: true});
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(off, TARGETED_USER_ID, PREMIUM).user_targeted).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,221 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
EXPERIMENT_BUCKET_RESOLUTION,
|
||||
type ExperimentTargeting,
|
||||
experimentAudienceIncludes,
|
||||
experimentBucket,
|
||||
} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {z} from 'zod';
|
||||
|
||||
export const VOICE_NOISE_SUPPRESSION_BACKENDS = [
|
||||
'none',
|
||||
'standard',
|
||||
'gate',
|
||||
'speex',
|
||||
'rnnoise',
|
||||
'gtcrn',
|
||||
'deep_filter',
|
||||
] as const;
|
||||
|
||||
export type VoiceNoiseSuppressionBackend = (typeof VOICE_NOISE_SUPPRESSION_BACKENDS)[number];
|
||||
|
||||
export const VoiceNoiseSuppressionBackendSchema = z.enum(VOICE_NOISE_SUPPRESSION_BACKENDS);
|
||||
|
||||
const VOICE_NOISE_SUPPRESSION_ROLLOUT_BASIS_POINTS_MAX = EXPERIMENT_BUCKET_RESOLUTION;
|
||||
const VOICE_NOISE_SUPPRESSION_MAX_TARGETED_USERS = 1000;
|
||||
const VOICE_NOISE_SUPPRESSION_MAX_GUILD_OVERRIDES = 200;
|
||||
const DEFAULT_VOICE_NOISE_SUPPRESSION_SALT = 'voice-ns-v1';
|
||||
|
||||
const TargetIdSchema = z.string().regex(/^\d{1,20}$/u);
|
||||
const TargetedUserIdsSchema = z.array(TargetIdSchema).max(VOICE_NOISE_SUPPRESSION_MAX_TARGETED_USERS);
|
||||
|
||||
const VoiceNoiseSuppressionGuildOverrideSchema = z.object({
|
||||
guild_id: TargetIdSchema,
|
||||
backend: VoiceNoiseSuppressionBackendSchema,
|
||||
});
|
||||
|
||||
const voiceConfigFields = {
|
||||
enabled: z.boolean(),
|
||||
config_version: z.number().int().min(0),
|
||||
default_backend: VoiceNoiseSuppressionBackendSchema,
|
||||
enabled_backends: z.array(VoiceNoiseSuppressionBackendSchema).max(VOICE_NOISE_SUPPRESSION_BACKENDS.length),
|
||||
allow_user_override: z.boolean(),
|
||||
rollout_basis_points: z.number().int().min(0).max(VOICE_NOISE_SUPPRESSION_ROLLOUT_BASIS_POINTS_MAX),
|
||||
rollout_salt: z.string().trim().min(1).max(64),
|
||||
included_user_ids: TargetedUserIdsSchema,
|
||||
included_guild_ids: TargetedUserIdsSchema,
|
||||
include_premium_users: z.boolean(),
|
||||
excluded_user_ids: TargetedUserIdsSchema,
|
||||
guild_overrides: z.array(VoiceNoiseSuppressionGuildOverrideSchema).max(VOICE_NOISE_SUPPRESSION_MAX_GUILD_OVERRIDES),
|
||||
suppression_strength: z.number().int().min(0).max(100),
|
||||
};
|
||||
|
||||
export const VoiceNoiseSuppressionConfigSchema = z.object({
|
||||
enabled: voiceConfigFields.enabled.default(false),
|
||||
config_version: voiceConfigFields.config_version.default(0),
|
||||
default_backend: voiceConfigFields.default_backend.default('standard'),
|
||||
enabled_backends: voiceConfigFields.enabled_backends.default([...VOICE_NOISE_SUPPRESSION_BACKENDS]),
|
||||
allow_user_override: voiceConfigFields.allow_user_override.default(true),
|
||||
rollout_basis_points: voiceConfigFields.rollout_basis_points.default(0),
|
||||
rollout_salt: voiceConfigFields.rollout_salt.default(DEFAULT_VOICE_NOISE_SUPPRESSION_SALT),
|
||||
included_user_ids: voiceConfigFields.included_user_ids.default([]),
|
||||
included_guild_ids: voiceConfigFields.included_guild_ids.default([]),
|
||||
include_premium_users: voiceConfigFields.include_premium_users.default(false),
|
||||
excluded_user_ids: voiceConfigFields.excluded_user_ids.default([]),
|
||||
guild_overrides: voiceConfigFields.guild_overrides.default([]),
|
||||
suppression_strength: voiceConfigFields.suppression_strength.default(80),
|
||||
});
|
||||
|
||||
export type VoiceNoiseSuppressionConfig = z.infer<typeof VoiceNoiseSuppressionConfigSchema>;
|
||||
|
||||
export const DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG: VoiceNoiseSuppressionConfig =
|
||||
VoiceNoiseSuppressionConfigSchema.parse({});
|
||||
|
||||
export const VoiceNoiseSuppressionConfigUpdateRequest = z
|
||||
.object(voiceConfigFields)
|
||||
.omit({config_version: true})
|
||||
.partial();
|
||||
|
||||
export type VoiceNoiseSuppressionConfigUpdateRequest = z.infer<typeof VoiceNoiseSuppressionConfigUpdateRequest>;
|
||||
|
||||
export const VoiceNoiseSuppressionConfigResponse = VoiceNoiseSuppressionConfigSchema;
|
||||
|
||||
export type VoiceNoiseSuppressionConfigResponse = z.infer<typeof VoiceNoiseSuppressionConfigResponse>;
|
||||
|
||||
const VOICE_NOISE_SUPPRESSION_ASSIGNMENT_SOURCES = ['user_rule', 'canary'] as const;
|
||||
|
||||
const VoiceNoiseSuppressionAssignmentSourceSchema = z.enum(VOICE_NOISE_SUPPRESSION_ASSIGNMENT_SOURCES);
|
||||
|
||||
export const VoiceNoiseSuppressionAssignmentResponse = z.object({
|
||||
enabled: voiceConfigFields.enabled,
|
||||
config_version: z.number().int(),
|
||||
user_targeted: z.boolean(),
|
||||
backend: VoiceNoiseSuppressionBackendSchema.nullable(),
|
||||
source: VoiceNoiseSuppressionAssignmentSourceSchema.nullable(),
|
||||
guild_overrides: z.array(VoiceNoiseSuppressionGuildOverrideSchema),
|
||||
enabled_backends: z.array(VoiceNoiseSuppressionBackendSchema),
|
||||
allow_user_override: voiceConfigFields.allow_user_override,
|
||||
suppression_strength: voiceConfigFields.suppression_strength,
|
||||
});
|
||||
|
||||
export type VoiceNoiseSuppressionAssignmentResponse = z.infer<typeof VoiceNoiseSuppressionAssignmentResponse>;
|
||||
|
||||
export const INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT: VoiceNoiseSuppressionAssignmentResponse = {
|
||||
enabled: false,
|
||||
config_version: 0,
|
||||
user_targeted: false,
|
||||
backend: null,
|
||||
source: null,
|
||||
guild_overrides: [],
|
||||
enabled_backends: [],
|
||||
allow_user_override: false,
|
||||
suppression_strength: DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG.suppression_strength,
|
||||
};
|
||||
|
||||
export function resolveVoiceNoiseSuppressionAssignment(
|
||||
config: VoiceNoiseSuppressionConfig,
|
||||
userId: string,
|
||||
targeting: ExperimentTargeting,
|
||||
): VoiceNoiseSuppressionAssignmentResponse {
|
||||
if (!config.enabled) {
|
||||
return {
|
||||
...INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT,
|
||||
config_version: config.config_version,
|
||||
};
|
||||
}
|
||||
const shared = {
|
||||
enabled: true,
|
||||
config_version: config.config_version,
|
||||
enabled_backends: [...config.enabled_backends],
|
||||
allow_user_override: config.allow_user_override,
|
||||
suppression_strength: config.suppression_strength,
|
||||
};
|
||||
if (config.excluded_user_ids.includes(userId)) {
|
||||
return {
|
||||
...shared,
|
||||
enabled_backends: [],
|
||||
allow_user_override: false,
|
||||
user_targeted: false,
|
||||
backend: null,
|
||||
source: null,
|
||||
guild_overrides: [],
|
||||
};
|
||||
}
|
||||
const backendIsUsable = config.enabled_backends.includes(config.default_backend);
|
||||
const guildOverrides = config.guild_overrides.filter((override) =>
|
||||
config.enabled_backends.includes(override.backend),
|
||||
);
|
||||
if (config.included_user_ids.includes(userId) || experimentAudienceIncludes(config, targeting)) {
|
||||
return {
|
||||
...shared,
|
||||
user_targeted: backendIsUsable,
|
||||
backend: backendIsUsable ? config.default_backend : null,
|
||||
source: backendIsUsable ? 'user_rule' : null,
|
||||
guild_overrides: guildOverrides,
|
||||
};
|
||||
}
|
||||
const inCanary = experimentBucket(userId, config.rollout_salt) < config.rollout_basis_points;
|
||||
return {
|
||||
...shared,
|
||||
user_targeted: inCanary && backendIsUsable,
|
||||
backend: inCanary && backendIsUsable ? config.default_backend : null,
|
||||
source: inCanary && backendIsUsable ? 'canary' : null,
|
||||
guild_overrides: guildOverrides,
|
||||
};
|
||||
}
|
||||
|
||||
export const VOICE_NOISE_SUPPRESSION_RESOLUTION_SOURCES = [
|
||||
'user_rule',
|
||||
'guild_rule',
|
||||
'canary',
|
||||
'user_override',
|
||||
] as const;
|
||||
|
||||
export type VoiceNoiseSuppressionResolutionSource = (typeof VOICE_NOISE_SUPPRESSION_RESOLUTION_SOURCES)[number];
|
||||
|
||||
export interface VoiceNoiseSuppressionResolution {
|
||||
backend: VoiceNoiseSuppressionBackend;
|
||||
source: VoiceNoiseSuppressionResolutionSource;
|
||||
suppressionStrength: number;
|
||||
configVersion: number;
|
||||
}
|
||||
|
||||
function resolveVoiceNoiseSuppressionTarget(
|
||||
assignment: VoiceNoiseSuppressionAssignmentResponse,
|
||||
guildId: string | null,
|
||||
): Pick<VoiceNoiseSuppressionResolution, 'backend' | 'source'> | null {
|
||||
if (assignment.source === 'user_rule' && assignment.backend != null) {
|
||||
return {backend: assignment.backend, source: 'user_rule'};
|
||||
}
|
||||
if (guildId != null) {
|
||||
const guildOverride = assignment.guild_overrides.find((override) => override.guild_id === guildId);
|
||||
if (guildOverride) return {backend: guildOverride.backend, source: 'guild_rule'};
|
||||
}
|
||||
if (!assignment.user_targeted) return null;
|
||||
if (assignment.backend == null) return null;
|
||||
if (assignment.source == null) return null;
|
||||
return {backend: assignment.backend, source: assignment.source};
|
||||
}
|
||||
|
||||
export function resolveVoiceNoiseSuppressionForCall(
|
||||
assignment: VoiceNoiseSuppressionAssignmentResponse,
|
||||
guildId: string | null,
|
||||
userPreference: VoiceNoiseSuppressionBackend | null,
|
||||
): VoiceNoiseSuppressionResolution | null {
|
||||
if (!assignment.enabled) return null;
|
||||
const targeted = resolveVoiceNoiseSuppressionTarget(assignment, guildId);
|
||||
if (targeted == null) return null;
|
||||
const shared = {
|
||||
suppressionStrength: assignment.suppression_strength,
|
||||
configVersion: assignment.config_version,
|
||||
};
|
||||
if (
|
||||
assignment.allow_user_override &&
|
||||
userPreference != null &&
|
||||
assignment.enabled_backends.includes(userPreference)
|
||||
) {
|
||||
return {...shared, backend: userPreference, source: 'user_override'};
|
||||
}
|
||||
return {...shared, backend: targeted.backend, source: targeted.source};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
const TARGETED_USER_ID = '1000000000000000001';
|
||||
const OTHER_USER_ID = '1000000000000000002';
|
||||
|
||||
function syntheticUserIds(count: number): Array<string> {
|
||||
const ids: Array<string> = [];
|
||||
for (let index = 0; index < count; index++) {
|
||||
ids.push((1400000000000000000n + BigInt(index)).toString());
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
describe('experimentBucket', () => {
|
||||
test('stays inside the basis point range for every synthetic id', () => {
|
||||
for (const userId of syntheticUserIds(500)) {
|
||||
const bucket = experimentBucket(userId, 'voice-ns-v1');
|
||||
expect(Number.isInteger(bucket)).toBe(true);
|
||||
expect(bucket).toBeGreaterThanOrEqual(0);
|
||||
expect(bucket).toBeLessThan(10000);
|
||||
}
|
||||
});
|
||||
|
||||
test.each([
|
||||
{userId: TARGETED_USER_ID, salt: 'voice-ns-v1', expected: 8241},
|
||||
{userId: TARGETED_USER_ID, salt: 'voice-ns-v2', expected: 8500},
|
||||
{userId: OTHER_USER_ID, salt: 'voice-ns-v1', expected: 5384},
|
||||
{userId: OTHER_USER_ID, salt: 'voice-ns-v2', expected: 1357},
|
||||
])('preserves the assignment bucket for $userId with salt $salt', ({userId, salt, expected}) => {
|
||||
expect(experimentBucket(userId, salt)).toBe(expected);
|
||||
});
|
||||
|
||||
test('changes with the salt for at least most user ids', () => {
|
||||
const userIds = syntheticUserIds(200);
|
||||
const changed = userIds.filter(
|
||||
(userId) => experimentBucket(userId, 'voice-ns-v1') !== experimentBucket(userId, 'voice-ns-v2'),
|
||||
);
|
||||
expect(changed.length).toBeGreaterThan(userIds.length - 5);
|
||||
});
|
||||
});
|
||||
@@ -9,10 +9,6 @@ import {
|
||||
INERT_PROFILE_TIMEZONE_ASSIGNMENT,
|
||||
ProfileTimezoneAssignmentResponse,
|
||||
} from '@fluxer/schema/src/domains/admin/ProfileTimezoneSchemas';
|
||||
import {
|
||||
INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT,
|
||||
VoiceNoiseSuppressionAssignmentResponse,
|
||||
} from '@fluxer/schema/src/domains/admin/VoiceNoiseSuppressionSchemas';
|
||||
import {z} from 'zod';
|
||||
|
||||
export const EXPERIMENT_MIN_POLL_INTERVAL_SECONDS = 60;
|
||||
@@ -50,7 +46,6 @@ export const ExperimentDeliveryConfigResponse = ExperimentDeliveryConfigSchema;
|
||||
export type ExperimentDeliveryConfigResponse = z.infer<typeof ExperimentDeliveryConfigResponse>;
|
||||
|
||||
const ExperimentAssignmentsSchema = z.object({
|
||||
voice_noise_suppression: VoiceNoiseSuppressionAssignmentResponse.optional(),
|
||||
domain_migration: DomainMigrationAssignmentResponse.optional(),
|
||||
altcha_captcha: AltchaCaptchaAssignmentResponse.optional(),
|
||||
profile_timezone: ProfileTimezoneAssignmentResponse.optional(),
|
||||
@@ -70,12 +65,6 @@ export const INERT_EXPERIMENT_ASSIGNMENTS_RESPONSE: ExperimentAssignmentsRespons
|
||||
assignments: {},
|
||||
};
|
||||
|
||||
export function readVoiceNoiseSuppressionAssignment(
|
||||
response: ExperimentAssignmentsResponse,
|
||||
): VoiceNoiseSuppressionAssignmentResponse {
|
||||
return response.assignments.voice_noise_suppression ?? INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT;
|
||||
}
|
||||
|
||||
export function readDomainMigrationAssignment(
|
||||
response: ExperimentAssignmentsResponse,
|
||||
): DomainMigrationAssignmentResponse {
|
||||
|
||||
@@ -42,8 +42,6 @@ function sameMicrophoneOptions(
|
||||
a?.echoCancellation === b?.echoCancellation &&
|
||||
a?.noiseSuppression === b?.noiseSuppression &&
|
||||
a?.autoGainControl === b?.autoGainControl &&
|
||||
a?.deepFilter === b?.deepFilter &&
|
||||
a?.deepFilterNoiseReductionLevel === b?.deepFilterNoiseReductionLevel &&
|
||||
a?.maxBitrateBps === b?.maxBitrateBps
|
||||
);
|
||||
}
|
||||
|
||||
@@ -179,8 +179,6 @@ export interface VoiceEngineV2MicrophoneOptions {
|
||||
echoCancellation?: boolean;
|
||||
noiseSuppression?: boolean;
|
||||
autoGainControl?: boolean;
|
||||
deepFilter?: boolean;
|
||||
deepFilterNoiseReductionLevel?: number;
|
||||
maxBitrateBps?: number;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user