mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +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
@@ -1,5 +1,13 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG,
|
||||
type VoiceNoiseSuppressionConfig,
|
||||
} from '@fluxer/schema/src/domains/admin/VoiceNoiseSuppressionSchemas';
|
||||
import {
|
||||
DEFAULT_EXPERIMENT_DELIVERY_CONFIG,
|
||||
type ExperimentDeliveryConfig,
|
||||
} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas';
|
||||
import {afterEach, describe, expect, it, vi} from 'vitest';
|
||||
import {setCassandraQueryExecutorForTesting} from '../database/CassandraQueryExecution';
|
||||
import type {PreparedQuery} from '../database/CassandraTypes';
|
||||
@@ -11,6 +19,9 @@ import {
|
||||
type InstanceRegistrationConfig,
|
||||
} from './InstanceConfigRepository';
|
||||
|
||||
const VOICE_NOISE_SUPPRESSION_CONFIG_KEY = 'voice_noise_suppression_config';
|
||||
const EXPERIMENT_DELIVERY_CONFIG_KEY = 'experiment_delivery_config';
|
||||
|
||||
class CountingInMemoryCassandraQueryExecutor extends InMemoryCassandraQueryExecutor {
|
||||
instanceConfigSelects = 0;
|
||||
|
||||
@@ -147,6 +158,148 @@ describe('InstanceConfigRepository', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the default voice noise suppression config when the key is absent', async () => {
|
||||
const executor = new CountingInMemoryCassandraQueryExecutor();
|
||||
setCassandraQueryExecutorForTesting(executor);
|
||||
const kvProvider = new MockKVProvider();
|
||||
const repository = createRepository(kvProvider);
|
||||
|
||||
await expect(repository.getVoiceNoiseSuppressionConfig()).resolves.toEqual(DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{name: 'unparseable text', stored: 'not-json'},
|
||||
{name: 'a json array', stored: '[]'},
|
||||
{name: 'out-of-range values', stored: '{"rollout_basis_points":99999}'},
|
||||
{name: 'an unknown backend', stored: '{"default_backend":"magic"}'},
|
||||
])('falls back to the default voice noise suppression config for $name', async ({stored}) => {
|
||||
const executor = new CountingInMemoryCassandraQueryExecutor();
|
||||
setCassandraQueryExecutorForTesting(executor);
|
||||
const kvProvider = new MockKVProvider();
|
||||
const repository = createRepository(kvProvider);
|
||||
|
||||
await repository.setConfig(VOICE_NOISE_SUPPRESSION_CONFIG_KEY, stored);
|
||||
|
||||
await expect(repository.getVoiceNoiseSuppressionConfig()).resolves.toEqual(DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG);
|
||||
});
|
||||
|
||||
it('round-trips a stored voice noise suppression config', async () => {
|
||||
const executor = new CountingInMemoryCassandraQueryExecutor();
|
||||
setCassandraQueryExecutorForTesting(executor);
|
||||
const kvProvider = new MockKVProvider();
|
||||
const repository = createRepository(kvProvider);
|
||||
|
||||
const config: VoiceNoiseSuppressionConfig = {
|
||||
...DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG,
|
||||
enabled: true,
|
||||
config_version: 3,
|
||||
default_backend: 'rnnoise',
|
||||
enabled_backends: ['none', 'standard', 'rnnoise'],
|
||||
allow_user_override: false,
|
||||
rollout_basis_points: 2500,
|
||||
rollout_salt: 'voice-ns-v2',
|
||||
included_user_ids: ['1400000000000000001'],
|
||||
excluded_user_ids: ['1400000000000000002'],
|
||||
guild_overrides: [{guild_id: '2400000000000000001', backend: 'rnnoise'}],
|
||||
stereo_enabled: true,
|
||||
suppression_strength: 55,
|
||||
};
|
||||
await repository.setVoiceNoiseSuppressionConfig(config);
|
||||
|
||||
await expect(repository.getVoiceNoiseSuppressionConfig()).resolves.toEqual(config);
|
||||
});
|
||||
|
||||
it('fills newly added voice noise suppression fields from the schema defaults', async () => {
|
||||
const executor = new CountingInMemoryCassandraQueryExecutor();
|
||||
setCassandraQueryExecutorForTesting(executor);
|
||||
const kvProvider = new MockKVProvider();
|
||||
const repository = createRepository(kvProvider);
|
||||
|
||||
await repository.setConfig(
|
||||
VOICE_NOISE_SUPPRESSION_CONFIG_KEY,
|
||||
JSON.stringify({enabled: true, config_version: 2, rollout_basis_points: 1000}),
|
||||
);
|
||||
|
||||
await expect(repository.getVoiceNoiseSuppressionConfig()).resolves.toEqual({
|
||||
...DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG,
|
||||
enabled: true,
|
||||
config_version: 2,
|
||||
rollout_basis_points: 1000,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the default experiment delivery config when the key is absent', async () => {
|
||||
const executor = new CountingInMemoryCassandraQueryExecutor();
|
||||
setCassandraQueryExecutorForTesting(executor);
|
||||
const kvProvider = new MockKVProvider();
|
||||
const repository = createRepository(kvProvider);
|
||||
|
||||
await expect(repository.getExperimentDeliveryConfig()).resolves.toEqual(DEFAULT_EXPERIMENT_DELIVERY_CONFIG);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{name: 'unparseable text', stored: 'not-json'},
|
||||
{name: 'a json array', stored: '[]'},
|
||||
{name: 'an out-of-range poll interval', stored: '{"poll_interval_seconds":1}'},
|
||||
{name: 'an out-of-range jitter', stored: '{"poll_jitter_percent":99}'},
|
||||
{name: 'a non-numeric poll interval', stored: '{"poll_interval_seconds":"often"}'},
|
||||
])('falls back to the default experiment delivery config for $name', async ({stored}) => {
|
||||
const executor = new CountingInMemoryCassandraQueryExecutor();
|
||||
setCassandraQueryExecutorForTesting(executor);
|
||||
const kvProvider = new MockKVProvider();
|
||||
const repository = createRepository(kvProvider);
|
||||
|
||||
await repository.setConfig(EXPERIMENT_DELIVERY_CONFIG_KEY, stored);
|
||||
|
||||
await expect(repository.getExperimentDeliveryConfig()).resolves.toEqual(DEFAULT_EXPERIMENT_DELIVERY_CONFIG);
|
||||
});
|
||||
|
||||
it('round-trips a stored experiment delivery config', async () => {
|
||||
const executor = new CountingInMemoryCassandraQueryExecutor();
|
||||
setCassandraQueryExecutorForTesting(executor);
|
||||
const kvProvider = new MockKVProvider();
|
||||
const repository = createRepository(kvProvider);
|
||||
|
||||
const config: ExperimentDeliveryConfig = {poll_interval_seconds: 900, poll_jitter_percent: 0};
|
||||
await repository.setExperimentDeliveryConfig(config);
|
||||
|
||||
await expect(repository.getExperimentDeliveryConfig()).resolves.toEqual(config);
|
||||
});
|
||||
|
||||
it('fills missing experiment delivery fields from the schema defaults', async () => {
|
||||
const executor = new CountingInMemoryCassandraQueryExecutor();
|
||||
setCassandraQueryExecutorForTesting(executor);
|
||||
const kvProvider = new MockKVProvider();
|
||||
const repository = createRepository(kvProvider);
|
||||
|
||||
await repository.setConfig(EXPERIMENT_DELIVERY_CONFIG_KEY, JSON.stringify({poll_interval_seconds: 3600}));
|
||||
|
||||
await expect(repository.getExperimentDeliveryConfig()).resolves.toEqual({
|
||||
...DEFAULT_EXPERIMENT_DELIVERY_CONFIG,
|
||||
poll_interval_seconds: 3600,
|
||||
});
|
||||
});
|
||||
|
||||
it('publishes a refresh so another repository observes the voice noise suppression config', async () => {
|
||||
const executor = new CountingInMemoryCassandraQueryExecutor();
|
||||
setCassandraQueryExecutorForTesting(executor);
|
||||
const kvProvider = new MockKVProvider();
|
||||
const reader = createRepository(kvProvider);
|
||||
const writer = createRepository(kvProvider);
|
||||
|
||||
await expect(reader.getVoiceNoiseSuppressionConfig()).resolves.toEqual(DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG);
|
||||
|
||||
await writer.setVoiceNoiseSuppressionConfig({
|
||||
...DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG,
|
||||
enabled: true,
|
||||
config_version: 1,
|
||||
});
|
||||
|
||||
await vi.waitFor(async () => {
|
||||
expect(await reader.getVoiceNoiseSuppressionConfig()).toMatchObject({enabled: true, config_version: 1});
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the registration URL id as the admin-visible registration code', async () => {
|
||||
const executor = new CountingInMemoryCassandraQueryExecutor();
|
||||
setCassandraQueryExecutorForTesting(executor);
|
||||
|
||||
@@ -6,6 +6,16 @@ import {
|
||||
type GatewayRolloutConfig,
|
||||
GatewayRolloutConfigSchema,
|
||||
} from '@fluxer/schema/src/domains/admin/GatewayRolloutSchemas';
|
||||
import {
|
||||
DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG,
|
||||
type VoiceNoiseSuppressionConfig,
|
||||
VoiceNoiseSuppressionConfigSchema,
|
||||
} from '@fluxer/schema/src/domains/admin/VoiceNoiseSuppressionSchemas';
|
||||
import {
|
||||
DEFAULT_EXPERIMENT_DELIVERY_CONFIG,
|
||||
type ExperimentDeliveryConfig,
|
||||
ExperimentDeliveryConfigSchema,
|
||||
} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas';
|
||||
import type {IKVProvider, IKVSubscription} from '@pkgs/kv_client/src/IKVProvider';
|
||||
import {Config} from '../Config';
|
||||
import type {APIConfig, BlueskyOAuthConfig, BlueskyOAuthKeyConfig} from '../config/APIConfig';
|
||||
@@ -21,6 +31,8 @@ import {getDefaultDateOfBirthCollection, setCachedDateOfBirthCollection} from '.
|
||||
import {normalizeSsoAllowedEmailDomains} from './SsoConfigValidation';
|
||||
|
||||
const GATEWAY_ROLLOUT_CONFIG_KEY = 'gateway_rollout_config';
|
||||
const VOICE_NOISE_SUPPRESSION_CONFIG_KEY = 'voice_noise_suppression_config';
|
||||
const EXPERIMENT_DELIVERY_CONFIG_KEY = 'experiment_delivery_config';
|
||||
const REGISTRATION_CONFIG_KEY = 'registration_config';
|
||||
const REGISTRATION_URLS_KEY = 'registration_urls';
|
||||
const REGISTRATION_PENDING_APPROVALS_KEY = 'registration_pending_approvals';
|
||||
@@ -43,6 +55,20 @@ const DEFAULT_GATEWAY_ROLLOUT_CONFIG: GatewayRolloutConfig = {
|
||||
gateway_dispatch_relay_max_queue: 50000,
|
||||
voice_e2ee_scope: 'guild_feature_only',
|
||||
};
|
||||
|
||||
function cloneDefaultVoiceNoiseSuppressionConfig(): VoiceNoiseSuppressionConfig {
|
||||
return {
|
||||
...DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG,
|
||||
enabled_backends: [...DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG.enabled_backends],
|
||||
included_user_ids: [...DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG.included_user_ids],
|
||||
excluded_user_ids: [...DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG.excluded_user_ids],
|
||||
guild_overrides: [...DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG.guild_overrides],
|
||||
};
|
||||
}
|
||||
|
||||
function cloneDefaultExperimentDeliveryConfig(): ExperimentDeliveryConfig {
|
||||
return {...DEFAULT_EXPERIMENT_DELIVERY_CONFIG};
|
||||
}
|
||||
export type InstanceRegistrationMode = 'open' | 'approval' | 'closed';
|
||||
export interface InstanceRegistrationConfig {
|
||||
mode: InstanceRegistrationMode;
|
||||
@@ -1050,6 +1076,48 @@ export class InstanceConfigRepository {
|
||||
await this.setConfig(GATEWAY_ROLLOUT_CONFIG_KEY, JSON.stringify(config));
|
||||
}
|
||||
|
||||
async getVoiceNoiseSuppressionConfig(): Promise<VoiceNoiseSuppressionConfig> {
|
||||
const raw = await this.getConfig(VOICE_NOISE_SUPPRESSION_CONFIG_KEY);
|
||||
if (!raw) {
|
||||
return cloneDefaultVoiceNoiseSuppressionConfig();
|
||||
}
|
||||
const parsed = parseJsonRecord(raw);
|
||||
if (!parsed) {
|
||||
return cloneDefaultVoiceNoiseSuppressionConfig();
|
||||
}
|
||||
const result = VoiceNoiseSuppressionConfigSchema.safeParse({...DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG, ...parsed});
|
||||
if (!result.success) {
|
||||
Logger.error({error: result.error}, 'Invalid voice noise suppression config');
|
||||
return cloneDefaultVoiceNoiseSuppressionConfig();
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
async setVoiceNoiseSuppressionConfig(config: VoiceNoiseSuppressionConfig): Promise<void> {
|
||||
await this.setConfig(VOICE_NOISE_SUPPRESSION_CONFIG_KEY, JSON.stringify(config));
|
||||
}
|
||||
|
||||
async getExperimentDeliveryConfig(): Promise<ExperimentDeliveryConfig> {
|
||||
const raw = await this.getConfig(EXPERIMENT_DELIVERY_CONFIG_KEY);
|
||||
if (!raw) {
|
||||
return cloneDefaultExperimentDeliveryConfig();
|
||||
}
|
||||
const parsed = parseJsonRecord(raw);
|
||||
if (!parsed) {
|
||||
return cloneDefaultExperimentDeliveryConfig();
|
||||
}
|
||||
const result = ExperimentDeliveryConfigSchema.safeParse({...DEFAULT_EXPERIMENT_DELIVERY_CONFIG, ...parsed});
|
||||
if (!result.success) {
|
||||
Logger.error({error: result.error}, 'Invalid experiment delivery config');
|
||||
return cloneDefaultExperimentDeliveryConfig();
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
async setExperimentDeliveryConfig(config: ExperimentDeliveryConfig): Promise<void> {
|
||||
await this.setConfig(EXPERIMENT_DELIVERY_CONFIG_KEY, JSON.stringify(config));
|
||||
}
|
||||
|
||||
async hasLimitConfig(): Promise<boolean> {
|
||||
const raw = await this.getConfig('limit_config');
|
||||
return raw !== null;
|
||||
|
||||
Reference in new issue
Block a user