mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +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,14 @@ import {
|
||||
GatewayRolloutConfigResponse,
|
||||
GatewayRolloutConfigUpdateRequest,
|
||||
} from '@fluxer/schema/src/domains/admin/GatewayRolloutSchemas';
|
||||
import {
|
||||
VoiceNoiseSuppressionConfigResponse,
|
||||
VoiceNoiseSuppressionConfigUpdateRequest,
|
||||
} from '@fluxer/schema/src/domains/admin/VoiceNoiseSuppressionSchemas';
|
||||
import {
|
||||
ExperimentDeliveryConfigResponse,
|
||||
ExperimentDeliveryConfigUpdateRequest,
|
||||
} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas';
|
||||
import {GuildMemberResponse} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
|
||||
import {MessageResponseSchema} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
import {ChannelTypeSchema} from '@fluxer/schema/src/primitives/ChannelValidators';
|
||||
@@ -629,6 +637,8 @@ const InstanceIntegrationsResponse = z.object({
|
||||
export const InstanceConfigResponse = z.object({
|
||||
sso: SsoConfigResponse,
|
||||
gateway_rollout: GatewayRolloutConfigResponse,
|
||||
voice_noise_suppression: VoiceNoiseSuppressionConfigResponse,
|
||||
experiment_delivery: ExperimentDeliveryConfigResponse,
|
||||
registration: InstanceRegistrationResponse,
|
||||
self_hosted: z.boolean(),
|
||||
app_public: AppPublicConfigResponse,
|
||||
@@ -641,6 +651,8 @@ export type InstanceConfigResponse = z.infer<typeof InstanceConfigResponse>;
|
||||
|
||||
export const InstanceConfigUpdateRequest = z.object({
|
||||
gateway_rollout: GatewayRolloutConfigUpdateRequest.nullish(),
|
||||
voice_noise_suppression: VoiceNoiseSuppressionConfigUpdateRequest.nullish(),
|
||||
experiment_delivery: ExperimentDeliveryConfigUpdateRequest.nullish(),
|
||||
registration: z
|
||||
.object({
|
||||
mode: RegistrationModeSchema.optional(),
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
// 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 VoiceNoiseSuppressionBackend,
|
||||
type VoiceNoiseSuppressionConfig,
|
||||
} from '@fluxer/schema/src/domains/admin/VoiceNoiseSuppressionSchemas';
|
||||
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';
|
||||
const GUILD_ID = '2000000000000000001';
|
||||
const OTHER_GUILD_ID = '2000000000000000002';
|
||||
|
||||
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,
|
||||
stereo_enabled: 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).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('is stable for the same user id and salt', () => {
|
||||
expect(experimentBucket(TARGETED_USER_ID, 'voice-ns-v1')).toBe(experimentBucket(TARGETED_USER_ID, 'voice-ns-v1'));
|
||||
});
|
||||
|
||||
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)).toEqual({
|
||||
...INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT,
|
||||
config_version: 4,
|
||||
});
|
||||
});
|
||||
|
||||
test('returns the inert assignment for the default config', () => {
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(createConfig(), TARGETED_USER_ID)).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);
|
||||
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);
|
||||
expect(assignment.user_targeted).toBe(false);
|
||||
expect(assignment.source).toBeNull();
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(config, OTHER_USER_ID).user_targeted).toBe(true);
|
||||
});
|
||||
|
||||
test('denylist strips guild overrides and the client-side knobs', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
allow_user_override: true,
|
||||
stereo_enabled: 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);
|
||||
expect(assignment.guild_overrides).toEqual([]);
|
||||
expect(assignment.enabled_backends).toEqual([]);
|
||||
expect(assignment.allow_user_override).toBe(false);
|
||||
expect(assignment.stereo_enabled).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);
|
||||
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);
|
||||
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);
|
||||
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).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).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).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'},
|
||||
])('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 as VoiceNoiseSuppressionBackend | null),
|
||||
).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',
|
||||
stereo_enabled: true,
|
||||
suppression_strength: 42,
|
||||
config_version: 19,
|
||||
});
|
||||
expect(resolveVoiceNoiseSuppressionForCall(assignment, null, null)).toEqual({
|
||||
backend: 'rnnoise',
|
||||
source: 'user_rule',
|
||||
stereoEnabled: true,
|
||||
suppressionStrength: 42,
|
||||
configVersion: 19,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,245 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {z} from 'zod';
|
||||
import {EXPERIMENT_BUCKET_RESOLUTION, experimentBucket} from '../experiment/ExperimentBucket';
|
||||
|
||||
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];
|
||||
|
||||
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 DEFAULT_VOICE_NOISE_SUPPRESSION_ENABLED_BACKENDS: ReadonlyArray<VoiceNoiseSuppressionBackend> = [
|
||||
'none',
|
||||
'standard',
|
||||
'gate',
|
||||
'speex',
|
||||
'rnnoise',
|
||||
'gtcrn',
|
||||
'deep_filter',
|
||||
];
|
||||
|
||||
const SnowflakeListSchema = (max: number) =>
|
||||
z
|
||||
.array(z.string().regex(/^\d{1,20}$/u))
|
||||
.max(max)
|
||||
.default([]);
|
||||
|
||||
const VoiceNoiseSuppressionGuildOverrideSchema = z.object({
|
||||
guild_id: z.string().regex(/^\d{1,20}$/u),
|
||||
backend: VoiceNoiseSuppressionBackendSchema,
|
||||
});
|
||||
|
||||
export const VoiceNoiseSuppressionConfigSchema = z.object({
|
||||
enabled: z.boolean().default(false),
|
||||
config_version: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
|
||||
default_backend: VoiceNoiseSuppressionBackendSchema.default('standard'),
|
||||
enabled_backends: z
|
||||
.array(VoiceNoiseSuppressionBackendSchema)
|
||||
.max(VOICE_NOISE_SUPPRESSION_BACKENDS.length)
|
||||
.default([...DEFAULT_VOICE_NOISE_SUPPRESSION_ENABLED_BACKENDS]),
|
||||
allow_user_override: z.boolean().default(true),
|
||||
rollout_basis_points: z.number().int().min(0).max(VOICE_NOISE_SUPPRESSION_ROLLOUT_BASIS_POINTS_MAX).default(0),
|
||||
rollout_salt: z.string().trim().min(1).max(64).default(DEFAULT_VOICE_NOISE_SUPPRESSION_SALT),
|
||||
included_user_ids: SnowflakeListSchema(VOICE_NOISE_SUPPRESSION_MAX_TARGETED_USERS),
|
||||
excluded_user_ids: SnowflakeListSchema(VOICE_NOISE_SUPPRESSION_MAX_TARGETED_USERS),
|
||||
guild_overrides: z
|
||||
.array(VoiceNoiseSuppressionGuildOverrideSchema)
|
||||
.max(VOICE_NOISE_SUPPRESSION_MAX_GUILD_OVERRIDES)
|
||||
.default([]),
|
||||
stereo_enabled: z.boolean().default(false),
|
||||
suppression_strength: z.number().int().min(0).max(100).default(80),
|
||||
});
|
||||
|
||||
export type VoiceNoiseSuppressionConfig = z.infer<typeof VoiceNoiseSuppressionConfigSchema>;
|
||||
|
||||
export const DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG: VoiceNoiseSuppressionConfig = {
|
||||
enabled: false,
|
||||
config_version: 0,
|
||||
default_backend: 'standard',
|
||||
enabled_backends: [...DEFAULT_VOICE_NOISE_SUPPRESSION_ENABLED_BACKENDS],
|
||||
allow_user_override: true,
|
||||
rollout_basis_points: 0,
|
||||
rollout_salt: DEFAULT_VOICE_NOISE_SUPPRESSION_SALT,
|
||||
included_user_ids: [],
|
||||
excluded_user_ids: [],
|
||||
guild_overrides: [],
|
||||
stereo_enabled: false,
|
||||
suppression_strength: 80,
|
||||
};
|
||||
|
||||
export const VoiceNoiseSuppressionConfigUpdateRequest = z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
default_backend: VoiceNoiseSuppressionBackendSchema.optional(),
|
||||
enabled_backends: z.array(VoiceNoiseSuppressionBackendSchema).max(VOICE_NOISE_SUPPRESSION_BACKENDS.length).optional(),
|
||||
allow_user_override: z.boolean().optional(),
|
||||
rollout_basis_points: z.number().int().min(0).max(VOICE_NOISE_SUPPRESSION_ROLLOUT_BASIS_POINTS_MAX).optional(),
|
||||
rollout_salt: z.string().trim().min(1).max(64).optional(),
|
||||
included_user_ids: z
|
||||
.array(z.string().regex(/^\d{1,20}$/u))
|
||||
.max(VOICE_NOISE_SUPPRESSION_MAX_TARGETED_USERS)
|
||||
.optional(),
|
||||
excluded_user_ids: z
|
||||
.array(z.string().regex(/^\d{1,20}$/u))
|
||||
.max(VOICE_NOISE_SUPPRESSION_MAX_TARGETED_USERS)
|
||||
.optional(),
|
||||
guild_overrides: z
|
||||
.array(VoiceNoiseSuppressionGuildOverrideSchema)
|
||||
.max(VOICE_NOISE_SUPPRESSION_MAX_GUILD_OVERRIDES)
|
||||
.optional(),
|
||||
stereo_enabled: z.boolean().optional(),
|
||||
suppression_strength: z.number().int().min(0).max(100).optional(),
|
||||
});
|
||||
|
||||
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: z.boolean(),
|
||||
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: z.boolean(),
|
||||
stereo_enabled: z.boolean(),
|
||||
suppression_strength: z.number().int().min(0).max(100),
|
||||
});
|
||||
|
||||
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,
|
||||
stereo_enabled: false,
|
||||
suppression_strength: DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG.suppression_strength,
|
||||
};
|
||||
|
||||
export function resolveVoiceNoiseSuppressionAssignment(
|
||||
config: VoiceNoiseSuppressionConfig,
|
||||
userId: string,
|
||||
): 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,
|
||||
stereo_enabled: config.stereo_enabled,
|
||||
suppression_strength: config.suppression_strength,
|
||||
};
|
||||
if (config.excluded_user_ids.includes(userId)) {
|
||||
return {
|
||||
...shared,
|
||||
enabled_backends: [],
|
||||
allow_user_override: false,
|
||||
stereo_enabled: 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)) {
|
||||
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;
|
||||
stereoEnabled: boolean;
|
||||
suppressionStrength: number;
|
||||
configVersion: number;
|
||||
}
|
||||
|
||||
export function resolveVoiceNoiseSuppressionForCall(
|
||||
assignment: VoiceNoiseSuppressionAssignmentResponse,
|
||||
guildId: string | null,
|
||||
userPreference: VoiceNoiseSuppressionBackend | null,
|
||||
): VoiceNoiseSuppressionResolution | null {
|
||||
if (!assignment.enabled) return null;
|
||||
const guildOverride =
|
||||
guildId == null ? undefined : assignment.guild_overrides.find((override) => override.guild_id === guildId);
|
||||
const targeted =
|
||||
assignment.source === 'user_rule' && assignment.backend != null
|
||||
? {backend: assignment.backend, source: 'user_rule' as const}
|
||||
: guildOverride != null
|
||||
? {backend: guildOverride.backend, source: 'guild_rule' as const}
|
||||
: assignment.user_targeted && assignment.backend != null && assignment.source != null
|
||||
? {backend: assignment.backend, source: assignment.source}
|
||||
: null;
|
||||
if (targeted == null) return null;
|
||||
const shared = {
|
||||
stereoEnabled: assignment.stereo_enabled,
|
||||
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,16 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
const FNV_OFFSET_BASIS_32 = 0x811c9dc5;
|
||||
const FNV_PRIME_32 = 0x01000193;
|
||||
|
||||
export const EXPERIMENT_BUCKET_RESOLUTION = 10000;
|
||||
|
||||
export function experimentBucket(userId: string, salt: string): number {
|
||||
let hash = FNV_OFFSET_BASIS_32;
|
||||
const input = `${salt}:${userId}`;
|
||||
for (let index = 0; index < input.length; index++) {
|
||||
hash ^= input.charCodeAt(index) & 0xff;
|
||||
hash = Math.imul(hash, FNV_PRIME_32) >>> 0;
|
||||
}
|
||||
return hash % EXPERIMENT_BUCKET_RESOLUTION;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {z} from 'zod';
|
||||
import {
|
||||
INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT,
|
||||
VoiceNoiseSuppressionAssignmentResponse,
|
||||
} from '../admin/VoiceNoiseSuppressionSchemas';
|
||||
|
||||
export const EXPERIMENT_MIN_POLL_INTERVAL_SECONDS = 60;
|
||||
export const EXPERIMENT_MAX_POLL_INTERVAL_SECONDS = 86400;
|
||||
export const EXPERIMENT_MAX_POLL_JITTER_PERCENT = 50;
|
||||
export const DEFAULT_EXPERIMENT_POLL_INTERVAL_SECONDS = 300;
|
||||
export const DEFAULT_EXPERIMENT_POLL_JITTER_PERCENT = 15;
|
||||
|
||||
export const ExperimentDeliveryConfigSchema = z.object({
|
||||
poll_interval_seconds: z
|
||||
.number()
|
||||
.int()
|
||||
.min(EXPERIMENT_MIN_POLL_INTERVAL_SECONDS)
|
||||
.max(EXPERIMENT_MAX_POLL_INTERVAL_SECONDS)
|
||||
.default(DEFAULT_EXPERIMENT_POLL_INTERVAL_SECONDS),
|
||||
poll_jitter_percent: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(EXPERIMENT_MAX_POLL_JITTER_PERCENT)
|
||||
.default(DEFAULT_EXPERIMENT_POLL_JITTER_PERCENT),
|
||||
});
|
||||
|
||||
export type ExperimentDeliveryConfig = z.infer<typeof ExperimentDeliveryConfigSchema>;
|
||||
|
||||
export const DEFAULT_EXPERIMENT_DELIVERY_CONFIG: ExperimentDeliveryConfig = {
|
||||
poll_interval_seconds: DEFAULT_EXPERIMENT_POLL_INTERVAL_SECONDS,
|
||||
poll_jitter_percent: DEFAULT_EXPERIMENT_POLL_JITTER_PERCENT,
|
||||
};
|
||||
|
||||
export const ExperimentDeliveryConfigUpdateRequest = z.object({
|
||||
poll_interval_seconds: z
|
||||
.number()
|
||||
.int()
|
||||
.min(EXPERIMENT_MIN_POLL_INTERVAL_SECONDS)
|
||||
.max(EXPERIMENT_MAX_POLL_INTERVAL_SECONDS)
|
||||
.optional(),
|
||||
poll_jitter_percent: z.number().int().min(0).max(EXPERIMENT_MAX_POLL_JITTER_PERCENT).optional(),
|
||||
});
|
||||
|
||||
export type ExperimentDeliveryConfigUpdateRequest = z.infer<typeof ExperimentDeliveryConfigUpdateRequest>;
|
||||
|
||||
export const ExperimentDeliveryConfigResponse = ExperimentDeliveryConfigSchema;
|
||||
|
||||
export type ExperimentDeliveryConfigResponse = z.infer<typeof ExperimentDeliveryConfigResponse>;
|
||||
|
||||
const ExperimentAssignmentsSchema = z.object({
|
||||
voice_noise_suppression: VoiceNoiseSuppressionAssignmentResponse.optional(),
|
||||
});
|
||||
|
||||
export const ExperimentAssignmentsResponse = z.object({
|
||||
poll_interval_seconds: z.number().int(),
|
||||
poll_jitter_percent: z.number().int().min(0).max(EXPERIMENT_MAX_POLL_JITTER_PERCENT),
|
||||
assignments: ExperimentAssignmentsSchema,
|
||||
});
|
||||
|
||||
export type ExperimentAssignmentsResponse = z.infer<typeof ExperimentAssignmentsResponse>;
|
||||
|
||||
export const INERT_EXPERIMENT_ASSIGNMENTS_RESPONSE: ExperimentAssignmentsResponse = {
|
||||
poll_interval_seconds: DEFAULT_EXPERIMENT_POLL_INTERVAL_SECONDS,
|
||||
poll_jitter_percent: DEFAULT_EXPERIMENT_POLL_JITTER_PERCENT,
|
||||
assignments: {},
|
||||
};
|
||||
|
||||
export function readVoiceNoiseSuppressionAssignment(
|
||||
response: ExperimentAssignmentsResponse,
|
||||
): VoiceNoiseSuppressionAssignmentResponse {
|
||||
return response.assignments.voice_noise_suppression ?? INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT;
|
||||
}
|
||||
Reference in new issue
Block a user