From 988cdac9f922d38a2f809c0aa3ed195011418998 Mon Sep 17 00:00:00 2001 From: Nikolai Giman Date: Mon, 13 Oct 2025 19:19:13 +0200 Subject: [PATCH] FIX: update settings --- packages/lib/package.json | 5 +- packages/lib/src/PalabraClient.model.ts | 8 +- packages/lib/src/PalabraClient.ts | 30 +-- .../lib/src/__tests__/PalabraClient.test.ts | 83 ++++++++ .../lib/src/config/PipelineConfig.model.ts | 59 +----- .../lib/src/config/PipelineConfigBuilder.ts | 91 ++++++--- .../lib/src/config/PipelineConfigManager.ts | 38 ++-- packages/lib/src/config/PipelineDefaults.ts | 55 +----- .../__test__/PipelineConfigBuilder.test.ts | 187 +++++++++++++++++- .../__test__/PipelineConfigManager.test.ts | 42 +++- .../config/__test__/PipelineDefaults.test.ts | 55 +----- pnpm-lock.yaml | 9 + 12 files changed, 433 insertions(+), 229 deletions(-) diff --git a/packages/lib/package.json b/packages/lib/package.json index 0f5cb8d..698a9f6 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -1,6 +1,6 @@ { "name": "@palabra-ai/translator", - "version": "0.0.5", + "version": "0.0.6", "private": false, "main": "dist/lib.js", "types": "dist/index.d.ts", @@ -20,7 +20,8 @@ }, "dependencies": { "livekit-client": "2.13.0", - "typed-emitter": "^2.1.0" + "typed-emitter": "^2.1.0", + "ts-deepmerge": "^7.0.3" }, "devDependencies": { "@eslint/js": "^9.28.0", diff --git a/packages/lib/src/PalabraClient.model.ts b/packages/lib/src/PalabraClient.model.ts index 7232a59..cf80600 100644 --- a/packages/lib/src/PalabraClient.model.ts +++ b/packages/lib/src/PalabraClient.model.ts @@ -1,5 +1,6 @@ import { SourceLangCode } from '~/utils/source'; import { TargetLangCode } from '~/utils/target'; +import { PipelineConfigManager } from './config/PipelineConfigManager'; export interface ClientCredentialsAuth { clientId: string; @@ -10,16 +11,19 @@ export interface UserTokenAuth { userToken: string; } -export interface PalabraClientData { + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface PalabraClientData = PipelineConfigManager> { auth:ClientCredentialsAuth | UserTokenAuth; translateFrom: SourceLangCode; translateTo: TargetLangCode; handleOriginalTrack: () => Promise; - transportType?: 'webrtc'; // TODO: add websocket transport | 'websocket' + transportType?: 'webrtc'; apiBaseUrl?: string; intent?:string; audioContext?:AudioContext; ignoreAudioContext?:boolean; + configManager?: CM; } export type TrackSid = string; \ No newline at end of file diff --git a/packages/lib/src/PalabraClient.ts b/packages/lib/src/PalabraClient.ts index 976e034..7471cb2 100644 --- a/packages/lib/src/PalabraClient.ts +++ b/packages/lib/src/PalabraClient.ts @@ -20,7 +20,8 @@ import { supportsAudioContextSetSinkId, VolumeNode } from './utils'; import { ConnectionState } from 'livekit-client'; import { PipelineConfig } from './config'; -export class PalabraClient extends PalabraBaseEventEmitter { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export class PalabraClient = PipelineConfigManager> extends PalabraBaseEventEmitter { private translateFrom: SourceLangCode; private translateTo: TargetLangCode; private auth: PalabraClientData['auth']; @@ -30,7 +31,7 @@ export class PalabraClient extends PalabraBaseEventEmitter { private originalTrackVolumeNode: VolumeNode | null = null; public transport: PalabraWebRtcTransport | null = null; private transportType: PalabraClientData['transportType']; - private configManager: PipelineConfigManager; + private configManager: CM; private audioContext: AudioContext; @@ -50,7 +51,7 @@ export class PalabraClient extends PalabraBaseEventEmitter { private ignoreAudioContext: PalabraClientData['ignoreAudioContext']; - constructor(data: PalabraClientData) { + constructor(data: PalabraClientData) { super(); this.auth = data.auth; @@ -61,20 +62,21 @@ export class PalabraClient extends PalabraBaseEventEmitter { this.transportType = data.transportType ?? 'webrtc'; - this.initConfig(); - this.shouldPlayTranslation = false; this.ignoreAudioContext = data.ignoreAudioContext ?? false; - if (data.audioContext) { - this.audioContext = data.audioContext; + if (data.configManager) { + this.configManager = data.configManager; } + + this.initConfig(); + + this.initAudioContext(data.audioContext); } public async startTranslation(): Promise { try { - this.initAudioContext(); await this.wrapOriginalTrack(); const transport = await this.createSession(); this.initTransportHandlers(); @@ -93,7 +95,6 @@ export class PalabraClient extends PalabraBaseEventEmitter { await this.deleteSession(); this.transport = null; this.stopPlayback(); - this.closeAudioContext(); this.cleanUnusedTracks([]); this.translationStatus = 'stopped'; this.cleanupOriginalTrack(); @@ -247,7 +248,7 @@ export class PalabraClient extends PalabraBaseEventEmitter { return this.transport; } - public getConfigManager() { + public getConfigManager(): CM { return this.configManager; } @@ -317,6 +318,7 @@ export class PalabraClient extends PalabraBaseEventEmitter { public async cleanup() { await this.stopTranslation(); + this.closeAudioContext(); this.initConfig(); } @@ -337,9 +339,9 @@ export class PalabraClient extends PalabraBaseEventEmitter { }); } - private async initAudioContext() { + private async initAudioContext(audioContext?: AudioContext) { if (this.audioContext || this.ignoreAudioContext) return; - this.audioContext = new AudioContext(); + this.audioContext = audioContext ?? new AudioContext(); } private closeAudioContext() { @@ -348,7 +350,9 @@ export class PalabraClient extends PalabraBaseEventEmitter { } private initConfig() { - this.configManager = new PipelineConfigManager(this.transportType); + if (!this.configManager) { + this.configManager = new PipelineConfigManager() as CM; + } this.configManager.setSourceLanguage(this.translateFrom as SourceLangCode); this.configManager.addTranslationTarget({ target_language: this.translateTo as TargetLangCode }); diff --git a/packages/lib/src/__tests__/PalabraClient.test.ts b/packages/lib/src/__tests__/PalabraClient.test.ts index e32042b..5c335da 100644 --- a/packages/lib/src/__tests__/PalabraClient.test.ts +++ b/packages/lib/src/__tests__/PalabraClient.test.ts @@ -3,6 +3,7 @@ import { PalabraClient } from '../PalabraClient'; import type { TargetLangCode } from '../utils/target'; import type { SourceLangCode } from '../utils/source'; import { EVENT_START_TRANSLATION, EVENT_STOP_TRANSLATION } from '../transport/PalabraWebRtcTransport.model'; +import { PipelineConfigManager } from '~/config'; // Mock MediaStreamTrack for tests class MockMediaStreamTrack { @@ -140,6 +141,88 @@ describe('PalabraClient', () => { client = new PalabraClient(baseConstructorData); }); + describe('ConfigManager', () => { + it('should set value and get value with extensions', ()=>{ + const manager = new PipelineConfigManager({ initialExtension: { testProp: 12 } }); + + const cl = new PalabraClient({ ...baseConstructorData, configManager: manager }); + expect((cl.getConfigManager()).getValue('testProp')).toBe(12); + + (cl.getConfigManager()).setValue('testProp', 13); + expect((cl.getConfigManager()).getValue('testProp')).toBe(13); + + manager.setValue('testProp', 14); + expect((cl.getConfigManager()).getValue('testProp')).toBe(14); + }); + + it('should set value and get value without extensions', ()=>{ + const cl = new PalabraClient({ ...baseConstructorData }); + expect((cl.getConfigManager()).getValue('preprocessing.enable_vad')).toBe(true); + }); + }); + + describe('AudioContext', () => { + it('should not close AudioContext in stopTranslation', async () => { + const closeAudioContextSpy = vi.spyOn(client as unknown as { closeAudioContext: () => void }, 'closeAudioContext').mockImplementation(() => undefined); + await client.stopTranslation(); + expect(closeAudioContextSpy).not.toHaveBeenCalled(); + }); + + it('should not call close method on AudioContext in stopTranslation', async() => { + const ctx = new AudioContext(); + const closeAudioContextSpy = vi.spyOn(ctx, 'close').mockImplementation(() => undefined); + const localClient = new PalabraClient({ ...baseConstructorData, audioContext: ctx }); + await localClient.stopTranslation(); + expect(closeAudioContextSpy).not.toHaveBeenCalled(); + }); + + it('should close AudioContext in cleanup', async () => { + const closeAudioContextSpy = vi.spyOn(client as unknown as { closeAudioContext: () => void }, 'closeAudioContext').mockImplementation(() => undefined); + await client.cleanup(); + expect(closeAudioContextSpy).toHaveBeenCalled(); + }); + + it('should ignore AudioContext creation when ignoreAudioContext is true and audioContext is provided', async() => { + const ctx = new AudioContext(); + const localClient = new PalabraClient({ ...baseConstructorData, audioContext: ctx, ignoreAudioContext: true }); + // @ts-expect-error: audioContext is private + expect(localClient.audioContext).toBeUndefined(); + }); + + it('should create a new AudioContext when ignoreAudioContext is false and audioContext is not provided', async() => { + const localClient = new PalabraClient({ ...baseConstructorData, ignoreAudioContext: false }); + // @ts-expect-error: audioContext is private + expect(localClient.audioContext).toBeDefined(); + }); + + it('should use provided AudioContext', async() => { + const ctx = new AudioContext(); + // @ts-expect-error field for test + ctx.field = 'test'; + + const localClient = new PalabraClient({ ...baseConstructorData, audioContext: ctx, ignoreAudioContext: false }); + // @ts-expect-error: audioContext is private + expect(localClient.audioContext).toBeDefined(); + // @ts-expect-error: field for test + expect(localClient.audioContext.field).toBe('test'); + }); + + it('should create a new AudioContext when audioContext is not provided', async() => { + const localClient = new PalabraClient(baseConstructorData); + await localClient.startTranslation(); + // @ts-expect-error: audioContext is private + expect(localClient.audioContext).toBeDefined(); + }); + }); + + it('should get api client', () => { + expect(client.getApiClient()).toBeDefined(); + }); + + it('should get config manager', () => { + expect(client.getConfigManager()).toBeDefined(); + }); + it('should create a new PalabraClient', () => { expect(client).toBeDefined(); }); diff --git a/packages/lib/src/config/PipelineConfig.model.ts b/packages/lib/src/config/PipelineConfig.model.ts index b14271e..1bb9132 100644 --- a/packages/lib/src/config/PipelineConfig.model.ts +++ b/packages/lib/src/config/PipelineConfig.model.ts @@ -6,6 +6,7 @@ export interface StreamConfigBase { } export interface StreamConfigWebRtc extends StreamConfigBase { + content_type: 'audio'; source?: { type: 'webrtc'; }; @@ -43,55 +44,18 @@ export interface PreprocessingConfig { export interface SentenceSplitterConfig { enabled: boolean; - splitter_model: 'auto' | string; - advanced: { - min_sentence_characters: number; - min_sentence_seconds: number; - min_split_interval: number; - context_size: number; - segments_after_restart: number; - step_size: number; - max_steps_without_eos: number; - force_end_of_segment: number; - }; } export interface VerificationConfig { - verification_model: 'auto' | string; - allow_verification_glossaries: boolean; auto_transcription_correction: boolean; transcription_correction_style: string | null; } - -export interface TranscriptionAdvancedConfig { - filler_phrases: { - enabled: boolean; - min_transcription_len: number; - min_transcription_time: number; - phrase_chance: number; - }; - ignore_languages: SourceLangCode[]; -} - export interface TranscriptionConfig { source_language: SourceLangCode; detectable_languages: SourceLangCode[]; - asr_model: 'auto' | string; - denoise: 'none' | string; - allow_hotwords_glossaries: boolean; - supress_numeral_tokens: boolean; - diarize_speakers: boolean; - priority: 'normal' | string; - min_alignment_score: number; - max_alignment_cer: number; segment_confirmation_silence_threshold: number; - only_confirm_by_silence: boolean; - batched_inference: boolean; - force_detect_language: boolean; - calculate_voice_loudness: boolean; sentence_splitter: SentenceSplitterConfig; verification: VerificationConfig; - advanced: TranscriptionAdvancedConfig; } export type AddTranslationArgs = Partial> & Pick; @@ -102,33 +66,15 @@ export interface VoiceTimbreDetectionConfig { low_timbre_voices: string[]; } -export interface SpeechGenerationAdvancedConfig { - f0_variance_factor: number; - energy_variance_factor: number; - with_custom_stress: boolean; -} - export interface SpeechGenerationConfig { - tts_model: 'auto' | string; voice_cloning: boolean; - voice_cloning_mode: 'static_10' | string; - denoise_voice_samples: boolean; voice_id: string; voice_timbre_detection: VoiceTimbreDetectionConfig; - speech_tempo_auto: boolean; - speech_tempo_timings_factor: number; - speech_tempo_adjustment_factor: number; - advanced: SpeechGenerationAdvancedConfig; } export interface TranslationConfig { target_language: TargetLangCode; - allowed_source_languages: SourceLangCode[]; - translation_model: 'auto' | 'alpha' | string; - allow_translation_glossaries: boolean; - style: string | null; translate_partial_transcriptions: boolean; - advanced: Record; speech_generation: SpeechGenerationConfig; } @@ -146,8 +92,7 @@ export type AllowedMessageTypes = (string | 'translated_transcription' | 'partial_translated_transcription' | 'partial_transcription' - | 'validated_transcription' - | 'pipeline_timings'); + | 'validated_transcription'); export interface PipelineConfig { input_stream: StreamConfig; diff --git a/packages/lib/src/config/PipelineConfigBuilder.ts b/packages/lib/src/config/PipelineConfigBuilder.ts index 680fbc1..de10a4d 100644 --- a/packages/lib/src/config/PipelineConfigBuilder.ts +++ b/packages/lib/src/config/PipelineConfigBuilder.ts @@ -5,6 +5,7 @@ import { PipelineConfig, PreprocessingConfig, TranscriptionConfig, + TranslationConfig, TranslationQueueConfig, TypeOfPropertyByPath, } from '~/config/PipelineConfig.model'; @@ -16,12 +17,31 @@ import { translation, } from '~/config/PipelineDefaults'; import { SourceLangCode } from '~/utils/source'; +import { merge } from 'ts-deepmerge'; -export class PipelineConfigBuilder { - private config: PipelineConfig; +type BasePipeline = PipelineConfig['pipeline']; - constructor() { - this.config = this.getDefaultWebRtcConfig(); +type CombinedPipeline = BasePipeline & T; + +export class PipelineConfigBuilder { + private config: Omit & { pipeline: CombinedPipeline }; + protected extension: T; + protected initialExtension: T; + + constructor(initialExtension?: T) { + this.extension = structuredClone(initialExtension); + this.initialExtension = structuredClone(initialExtension); + this.config = this.getMergedConfig(); + } + + private getMergedConfig(restoreDefaults = false) { + const baseConfig = this.getDefaultWebRtcConfig(); + + const config = { + ...baseConfig, + pipeline: merge(baseConfig.pipeline, restoreDefaults ? this.initialExtension : this.extension) as CombinedPipeline, + }; + return config; } private getDefaultWebRtcConfig(): PipelineConfig { @@ -79,17 +99,21 @@ export class PipelineConfigBuilder { } public restoreDefaults() { - this.config = this.getDefaultWebRtcConfig(); + this.config = this.getMergedConfig(true); return this; } public useWebSocket(): this { - this.config = this.getDefaultWebSocketConfig(); + const base = this.getDefaultWebSocketConfig(); + this.config = { + ...base, + pipeline: merge(base.pipeline, this.extension) as CombinedPipeline, + }; return this; } public useWebRTC(): this { - this.config = this.getDefaultWebRtcConfig(); + this.config = this.getMergedConfig(); return this; } @@ -137,10 +161,7 @@ export class PipelineConfigBuilder { * @returns this */ public setTranscription(config: Partial): this { - this.config.pipeline.transcription = { - ...this.config.pipeline.transcription, - ...structuredClone(config), - }; + this.config.pipeline.transcription = merge(this.config.pipeline.transcription, config); return this; } @@ -151,10 +172,7 @@ export class PipelineConfigBuilder { * @returns this */ public addTranslation(config: AddTranslationArgs): this { - this.config.pipeline.translations.push({ - ...translation, - ...structuredClone(config), - }); + this.config.pipeline.translations.push(merge(translation, config) as TranslationConfig); return this; } @@ -170,10 +188,7 @@ export class PipelineConfigBuilder { * @returns this */ public setTranslationQueue(config: Partial): this { - this.config.pipeline.translation_queue_configs = { - ...this.config.pipeline.translation_queue_configs, - ...structuredClone(config), - }; + this.config.pipeline.translation_queue_configs = merge(this.config.pipeline.translation_queue_configs, config); return this; } @@ -210,8 +225,8 @@ export class PipelineConfigBuilder { return this; } - public setPipeline(newPipeline: PipelineConfig['pipeline']): PipelineConfig['pipeline'] { - this.config.pipeline = structuredClone(newPipeline); + public setPipeline(newPipeline: CombinedPipeline): CombinedPipeline { + this.config.pipeline = structuredClone(newPipeline) as CombinedPipeline; return structuredClone(this.config.pipeline); } @@ -219,7 +234,7 @@ export class PipelineConfigBuilder { * Build pipeline config * @returns PipelineConfig */ - public build(): PipelineConfig { + public build(): PipelineConfig & { pipeline: CombinedPipeline } { return structuredClone(this.config); } @@ -228,22 +243,40 @@ export class PipelineConfigBuilder { * @param config - PipelineConfig * @returns PipelineConfigBuilder */ - public static fromConfig(config: PipelineConfig): PipelineConfigBuilder { - const builder = new PipelineConfigBuilder(); - builder.config = structuredClone(config); + public static fromConfig = Record>(config: PipelineConfig, initialExtension?: E): PipelineConfigBuilder { + const builder = new PipelineConfigBuilder(initialExtension); + builder.config = { + ...structuredClone(config), + pipeline: merge(config.pipeline, initialExtension ?? {}) as CombinedPipeline, + }; return builder; } - public setValue

>(path: P, value: TypeOfPropertyByPath) { + public setValue

>(path: P, value: TypeOfPropertyByPath) { const keys = path.split('.'); const lastKey = keys.pop(); let parentProp: PipelineConfig['pipeline'] = this.config.pipeline; - keys.forEach(k => parentProp = parentProp[k]); + keys.forEach((key, index) => { + const nextKey = keys[index + 1]; + const isNextKeyNumeric = /^\d+$/.test(nextKey ?? lastKey); + const isCurrentKeyNumeric = /^\d+$/.test(key); + + if (isCurrentKeyNumeric && !Array.isArray(parentProp)) { + throw new Error(`Cannot set array index "${key}" on non-array at "${keys.slice(0, index).join('.')}"`); + } + + if (parentProp[key] === undefined || parentProp[key] === null) { + parentProp[key] = isNextKeyNumeric ? [] : {}; + } + + parentProp = parentProp[key]; + }); + parentProp[lastKey] = value; return this.config.pipeline; } - public getValue

>(path: P): TypeOfPropertyByPath { - return path.split('.').reduce((acc, key) => acc[key], this.config.pipeline); + public getValue

>(path: P): TypeOfPropertyByPath | undefined { + return path.split('.').reduce((acc, key) => !acc ? undefined : acc[key], this.config.pipeline); } } \ No newline at end of file diff --git a/packages/lib/src/config/PipelineConfigManager.ts b/packages/lib/src/config/PipelineConfigManager.ts index f4be0d7..386d556 100644 --- a/packages/lib/src/config/PipelineConfigManager.ts +++ b/packages/lib/src/config/PipelineConfigManager.ts @@ -2,12 +2,17 @@ import { PipelineConfigBuilder } from '~/config/PipelineConfigBuilder'; import { SourceLangCode } from '~/utils/source'; import { translation as defaultTranslation } from './PipelineDefaults'; import { AddTranslationArgs, AllowedMessageTypes, AvailablePaths, PipelineConfig, TypeOfPropertyByPath } from './PipelineConfig.model'; +import { merge } from 'ts-deepmerge'; -export class PipelineConfigManager { - private builder: PipelineConfigBuilder; +export class PipelineConfigManager { + private builder: PipelineConfigBuilder; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private extension?: {initialExtension?: T, translationExtension?: Record}; - constructor(type: 'webrtc' = 'webrtc') { - this.builder = new PipelineConfigBuilder(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(extension?: {initialExtension?: T, translationExtension?: Record}, type: 'webrtc' = 'webrtc') { + this.builder = new PipelineConfigBuilder(extension?.initialExtension); + this.extension = extension; if (type === 'webrtc') { this.builder.useWebRTC(); } else { @@ -15,9 +20,15 @@ export class PipelineConfigManager { } } - public static fromConfig(config: PipelineConfig, type: 'webrtc'): PipelineConfigManager { - const manager = new PipelineConfigManager(type); - manager.builder = PipelineConfigBuilder.fromConfig(config); + public static fromConfig = Record>( + config: PipelineConfig, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + extension: {initialExtension?: E, translationExtension?: Record}, + type: 'webrtc' = 'webrtc', + ): PipelineConfigManager + { + const manager = new PipelineConfigManager(extension, type); + manager.builder = PipelineConfigBuilder.fromConfig(config, extension.initialExtension); return manager; } @@ -27,10 +38,7 @@ export class PipelineConfigManager { } public addTranslationTarget(config: AddTranslationArgs): this { - this.builder.addTranslation({ - ...defaultTranslation, - ...config, - }); + this.builder.addTranslation(merge(defaultTranslation, this.extension?.translationExtension, config) as unknown as AddTranslationArgs); return this; } @@ -44,7 +52,7 @@ export class PipelineConfigManager { return this; } - public getConfig(): PipelineConfig { + public getConfig() { return this.builder.build(); } @@ -52,7 +60,7 @@ export class PipelineConfigManager { return structuredClone(this.getConfig().pipeline); } - public setJSON(newPipeline: PipelineConfig['pipeline']): PipelineConfig['pipeline'] { + public setJSON(newPipeline: PipelineConfig['pipeline'] & T): PipelineConfig['pipeline'] & T { return this.builder.setPipeline(newPipeline); } @@ -61,11 +69,11 @@ export class PipelineConfigManager { return structuredClone(this.getConfig().pipeline); } - public setValue

>(path: P, value: TypeOfPropertyByPath) { + public setValue

>(path: P, value: TypeOfPropertyByPath) { return this.builder.setValue(path, value); } - public getValue

>(path: P): TypeOfPropertyByPath { + public getValue

>(path: P): TypeOfPropertyByPath { return this.builder.getValue(path); } } \ No newline at end of file diff --git a/packages/lib/src/config/PipelineDefaults.ts b/packages/lib/src/config/PipelineDefaults.ts index 919004f..cc874e4 100644 --- a/packages/lib/src/config/PipelineDefaults.ts +++ b/packages/lib/src/config/PipelineDefaults.ts @@ -5,31 +5,15 @@ import { PipelineConfig, PreprocessingConfig, TranscriptionConfig, TranslationCo * @link https://docs.palabra.ai/docs/streaming_api/translation_settings_breakdown/#233-translations */ export const translation: Omit = { - allowed_source_languages: [], - translation_model: 'auto', - allow_translation_glossaries: true, - style: null, translate_partial_transcriptions: false, - advanced: {}, speech_generation: { - tts_model: 'auto', voice_cloning: false, - voice_cloning_mode: 'static_10', - denoise_voice_samples: true, voice_id: 'default_low', voice_timbre_detection: { enabled: false, high_timbre_voices: ['default_high'], low_timbre_voices: ['default_low'], }, - speech_tempo_auto: true, - speech_tempo_timings_factor: 0, - speech_tempo_adjustment_factor: 0.75, - advanced: { - f0_variance_factor: 1.2, - energy_variance_factor: 1.5, - with_custom_stress: true, - }, }, }; @@ -54,48 +38,14 @@ export const preprocessing: PreprocessingConfig = { export const transcription: TranscriptionConfig = { source_language: 'en', detectable_languages: [], - asr_model: 'auto', - denoise: 'none', - allow_hotwords_glossaries: true, - supress_numeral_tokens: false, - diarize_speakers: false, - priority: 'normal', - min_alignment_score: 0.2, - max_alignment_cer: 0.8, segment_confirmation_silence_threshold: 0.7, - only_confirm_by_silence: false, - batched_inference: false, - force_detect_language: false, - calculate_voice_loudness: false, sentence_splitter: { enabled: true, - splitter_model: 'auto', - advanced: { - min_sentence_characters: 80, - min_sentence_seconds: 4, - min_split_interval: 0.6, - context_size: 30, - segments_after_restart: 15, - step_size: 5, - max_steps_without_eos: 3, - force_end_of_segment: 0.5, - }, }, verification: { - verification_model: 'auto', - allow_verification_glossaries: true, auto_transcription_correction: false, transcription_correction_style: null, }, - advanced: { - filler_phrases: { - enabled: false, - min_transcription_len: 40, - min_transcription_time: 3, - phrase_chance: 0.5, - }, - ignore_languages: [], - }, }; @@ -108,8 +58,8 @@ export const translation_queue_configs: TranslationQueueConfig = { desired_queue_level_ms: 10000, max_queue_level_ms: 24000, auto_tempo: true, - min_tempo: 1.0, - max_tempo: 1.2, + min_tempo: 1.15, + max_tempo: 1.45, }, }; @@ -122,5 +72,4 @@ export const allowed_message_types: PipelineConfig['pipeline']['allowed_message_ 'partial_translated_transcription', 'partial_transcription', 'validated_transcription', - 'pipeline_timings', ]; \ No newline at end of file diff --git a/packages/lib/src/config/__test__/PipelineConfigBuilder.test.ts b/packages/lib/src/config/__test__/PipelineConfigBuilder.test.ts index 954004a..0ed848a 100644 --- a/packages/lib/src/config/__test__/PipelineConfigBuilder.test.ts +++ b/packages/lib/src/config/__test__/PipelineConfigBuilder.test.ts @@ -154,6 +154,34 @@ describe('PipelineConfigBuilder WebRtc', () => { const config2 = builder.build(); expect(config2).toEqual(config); }); + + it('From config should create a config builder with extensions', () => { + const config: PipelineConfig & {pipeline: {testProp: number}} = { + input_stream: { + content_type: 'audio', + source: { + type: 'webrtc', + }, + }, + output_stream: { + content_type: 'audio', + target: { + type: 'webrtc', + }, + }, + pipeline: { + preprocessing, + transcription, + translations: [], + translation_queue_configs, + allowed_message_types, + testProp: 111, + }, + }; + const builder = PipelineConfigBuilder.fromConfig(config, { testProp: 111 }); + const config2 = builder.build(); + expect(config2).toEqual(config); + }); }); @@ -212,9 +240,162 @@ describe('PipelineConfigBuilder set and get', () => { expect(builder.getValue('undefined_property')).toBeUndefined(); }); - it('setValue & getValue should work with nested paths', () => { + it('getValue should return undefined if nested property is not defined', () => { const builder = new PipelineConfigBuilder(); - builder.setValue('preprocessing.enable_vad', false); - expect(builder.getValue('preprocessing.enable_vad')).toEqual(false); + // @ts-expect-error - This is a test + expect(builder.getValue('undefined_property.test.nested')).toBeUndefined(); + }); + + it('setValue & getValue should work with extensions', () => { + const builder = new PipelineConfigBuilder({ preprocessing: { testProp: 111 } }); + expect(builder.getValue('preprocessing.testProp')).toEqual(111); + }); + + it('setPipeline should work with extensions and build should return the config with the extensions', () => { + const builder = new PipelineConfigBuilder({ preprocessing: { testProp: 111 } }); + builder.setPipeline({ + preprocessing: { + ...preprocessing, + testProp: 222, + }, + transcription, + translations: [], + translation_queue_configs, + allowed_message_types, + }); + expect(builder.getValue('preprocessing.testProp')).toEqual(222); + const config = builder.build(); + expect(config.pipeline.preprocessing.testProp).toEqual(222); + }); + + it('should restore defaults', () => { + const builder = new PipelineConfigBuilder({ preprocessing: { testProp: 111 } }); + builder.setValue('preprocessing.testProp', 222); + builder.restoreDefaults(); + expect(builder.getValue('preprocessing.testProp')).toEqual(111); + const config = builder.build(); + expect(config.pipeline.preprocessing.testProp).toEqual(111); + }); + + + + describe('setValue for new path which is not defined', () => { + it('should setValue for new path which is not defined', () => { + const builder = new PipelineConfigBuilder(); + const p1 = 'preprocessingNew.testNew'; + // @ts-expect-error - This is a test + builder.setValue(p1, 222); + // @ts-expect-error - This is a test + expect(builder.getValue(p1)).toEqual(222); + }); + + it('should setValue for new path which is not defined and is array', () => { + const builder = new PipelineConfigBuilder(); + const p1 = 'preprocessingNew.testNew.0'; + const p2 = 'preprocessingNew.testNew.1'; + // @ts-expect-error - This is a test + builder.setValue(p1, { obj: 123 }); + // @ts-expect-error - This is a test + expect(builder.getValue(p1)).toEqual({ obj: 123 }); + // @ts-expect-error - This is a test + expect(builder.getValue('preprocessingNew.testNew')).toBeInstanceOf(Array); + + // @ts-expect-error - This is a test + builder.setValue(p2, { obj: 456 }); + // @ts-expect-error - This is a test + expect(builder.getValue(p2)).toEqual({ obj: 456 }); + // @ts-expect-error - This is a test + expect(builder.getValue('preprocessingNew.testNew')).toBeInstanceOf(Array); + // @ts-expect-error - This is a test + expect(builder.getValue('preprocessingNew.testNew')[1]).toEqual({ obj: 456 }); + }); + + it('should setValue for new path which is not defined and is object', () => { + const builder = new PipelineConfigBuilder(); + const p1 = 'preprocessingNew.testNew'; + // @ts-expect-error - This is a test + builder.setValue(p1, { obj: 123 }); + // @ts-expect-error - This is a test + expect(builder.getValue(p1)).toEqual({ obj: 123 }); + // @ts-expect-error - This is a test + expect(builder.getValue('preprocessingNew.testNew')).toBeInstanceOf(Object); + // @ts-expect-error - This is a test + expect(builder.getValue('preprocessingNew.testNew').obj).toEqual(123); + // @ts-expect-error - This is a test + builder.setValue('preprocessingNew.testNew.obj', 555); + // @ts-expect-error - This is a test + expect(builder.getValue('preprocessingNew.testNew.obj')).toEqual(555); + // @ts-expect-error - This is a test + builder.setValue('preprocessingNew.testNew.nts', { test: 555 }); + // @ts-expect-error - This is a test + expect(builder.getValue('preprocessingNew.testNew')).toEqual({ + nts: { + test: 555, + }, + obj: 555, + }); + }); + + it('should setValue throw error if we try set array index on non-array', () => { + const builder = new PipelineConfigBuilder(); + const p1 = 'preprocessingNew.testNew'; + // @ts-expect-error - This is a test + builder.setValue(p1, { obj: 123 }); + // @ts-expect-error - This is a test + expect(builder.getValue(p1)).toEqual({ obj: 123 }); + // @ts-expect-error - This is a test + expect(()=>builder.setValue('preprocessingNew.0.0', 555)).toThrow(); + }); + + it('should setValue not throw error if we try set by numeric index', () => { + const builder = new PipelineConfigBuilder(); + const p1 = 'preprocessingNew.testNew'; + // @ts-expect-error - This is a test + builder.setValue(p1, { obj: 123 }); + // @ts-expect-error - This is a test + expect(builder.getValue(p1)).toEqual({ obj: 123 }); + // @ts-expect-error - This is a test + expect(()=>builder.setValue('preprocessingNew."0".0', 555)).not.toThrow(); + // @ts-expect-error - This is a test + expect(builder.getValue('preprocessingNew."0".0')).toEqual(555); + }); + }); + + describe('deepMerge', () => { + it('should return default config for webrtc', () => { + const builder = new PipelineConfigBuilder(); + const config = builder.build(); + expect(config).toEqual({ + input_stream: { + content_type: 'audio', + source: { + type: 'webrtc', + }, + }, + output_stream: { + content_type: 'audio', + target: { + type: 'webrtc', + }, + }, + pipeline: { + preprocessing, + transcription, + translations: [], + translation_queue_configs, + allowed_message_types, + }, + }); + }); + + it('should return default with merged extensions', () => { + const builder = new PipelineConfigBuilder({ preprocessing: { testProp: 111 } }); + const config = builder.build(); + expect(config.pipeline.preprocessing.testProp).toEqual(111); + expect(config.pipeline.preprocessing).toEqual({ + ...preprocessing, + testProp: 111, + }); + }); }); }); \ No newline at end of file diff --git a/packages/lib/src/config/__test__/PipelineConfigManager.test.ts b/packages/lib/src/config/__test__/PipelineConfigManager.test.ts index 0d3b98f..2cd4d61 100644 --- a/packages/lib/src/config/__test__/PipelineConfigManager.test.ts +++ b/packages/lib/src/config/__test__/PipelineConfigManager.test.ts @@ -4,10 +4,38 @@ import { preprocessing, transcription, translation_queue_configs, allowed_messag import { PipelineConfig } from '~/config/PipelineConfig.model'; describe('PipelineConfigManager', () => { - let manager: PipelineConfigManager; + let manager: PipelineConfigManager<{ ff: number }>; beforeEach(() => { manager = new PipelineConfigManager(); }); + + it('Should create a config manager with extensions', () => { + const manager = new PipelineConfigManager({ initialExtension: { additional_config: 1 } }); + const config = manager.getConfig(); + expect(config).toEqual({ + input_stream: { + content_type: 'audio', + source: { + type: 'webrtc', + }, + }, + output_stream: { + content_type: 'audio', + target: { + type: 'webrtc', + }, + }, + pipeline: { + preprocessing, + transcription, + translations: [], + translation_queue_configs, + allowed_message_types, + additional_config: 1, + }, + }); + }); + it('Should create a default config with WebRTC input and output', () => { const config = manager.getConfig(); expect(config).toEqual({ @@ -55,7 +83,7 @@ describe('PipelineConfigManager', () => { allowed_message_types: ['translated_transcription', 'partial_translated_transcription'], }, }; - const manager = PipelineConfigManager.fromConfig(config, 'webrtc'); + const manager = PipelineConfigManager.fromConfig(config, {}, 'webrtc'); const config2 = manager.getConfig(); expect(config2).toEqual(config); }); @@ -117,4 +145,14 @@ describe('PipelineConfigManager', () => { }, }); }); + + it('should restore defaults', () => { + const manager = new PipelineConfigManager({ initialExtension: { preprocessing: { testProp: 111 } } }); + manager.setValue('preprocessing.testProp', 222); + expect(manager.getValue('preprocessing.testProp')).toEqual(222); + manager.restoreDefaults(); + expect(manager.getValue('preprocessing.testProp')).toEqual(111); + const config = manager.getConfig(); + expect(config.pipeline.preprocessing.testProp).toEqual(111); + }); }); \ No newline at end of file diff --git a/packages/lib/src/config/__test__/PipelineDefaults.test.ts b/packages/lib/src/config/__test__/PipelineDefaults.test.ts index db1ce10..75cf39a 100644 --- a/packages/lib/src/config/__test__/PipelineDefaults.test.ts +++ b/packages/lib/src/config/__test__/PipelineDefaults.test.ts @@ -5,31 +5,15 @@ import { AllowedMessageTypes } from '~/config/PipelineConfig.model'; describe('PipelineDefaults', () => { it('Translation default object should match the default config', () => { expect(configDefaults.translation).toEqual({ - allowed_source_languages: [], - translation_model: 'auto', - allow_translation_glossaries: true, - style: null, translate_partial_transcriptions: false, - advanced: {}, speech_generation: { - tts_model: 'auto', voice_cloning: false, - voice_cloning_mode: 'static_10', - denoise_voice_samples: true, voice_id: 'default_low', voice_timbre_detection: { enabled: false, high_timbre_voices: ['default_high'], low_timbre_voices: ['default_low'], }, - speech_tempo_auto: true, - speech_tempo_timings_factor: 0, - speech_tempo_adjustment_factor: 0.75, - advanced: { - f0_variance_factor: 1.2, - energy_variance_factor: 1.5, - with_custom_stress: true, - }, }, }); }); @@ -50,48 +34,14 @@ describe('PipelineDefaults', () => { expect(configDefaults.transcription).toEqual({ source_language: 'en', detectable_languages: [], - asr_model: 'auto', - denoise: 'none', - allow_hotwords_glossaries: true, - supress_numeral_tokens: false, - diarize_speakers: false, - priority: 'normal', - min_alignment_score: 0.2, - max_alignment_cer: 0.8, segment_confirmation_silence_threshold: 0.7, - only_confirm_by_silence: false, - batched_inference: false, - force_detect_language: false, - calculate_voice_loudness: false, sentence_splitter: { enabled: true, - splitter_model: 'auto', - advanced: { - min_sentence_characters: 80, - min_sentence_seconds: 4, - min_split_interval: 0.6, - context_size: 30, - segments_after_restart: 15, - step_size: 5, - max_steps_without_eos: 3, - force_end_of_segment: 0.5, - }, }, verification: { - verification_model: 'auto', - allow_verification_glossaries: true, auto_transcription_correction: false, transcription_correction_style: null, }, - advanced: { - filler_phrases: { - enabled: false, - min_transcription_len: 40, - min_transcription_time: 3, - phrase_chance: 0.5, - }, - ignore_languages: [], - }, }); }); @@ -101,8 +51,8 @@ describe('PipelineDefaults', () => { desired_queue_level_ms: 10000, max_queue_level_ms: 24000, auto_tempo: true, - min_tempo: 1.0, - max_tempo: 1.2, + min_tempo: 1.15, + max_tempo: 1.45, }, }); }); @@ -113,7 +63,6 @@ describe('PipelineDefaults', () => { 'partial_translated_transcription', 'partial_transcription', 'validated_transcription', - 'pipeline_timings', ]); }); it('Allowed message types should only contain valid types', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4b8fac..0286b16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: livekit-client: specifier: 2.13.0 version: 2.13.0 + ts-deepmerge: + specifier: ^7.0.3 + version: 7.0.3 typed-emitter: specifier: ^2.1.0 version: 2.1.0 @@ -2541,6 +2544,10 @@ packages: ts-debounce@4.0.0: resolution: {integrity: sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==} + ts-deepmerge@7.0.3: + resolution: {integrity: sha512-Du/ZW2RfwV/D4cmA5rXafYjBQVuvu4qGiEEla4EmEHVHgRdx68Gftx7i66jn2bzHPwSVZY36Ae6OuDn9el4ZKA==} + engines: {node: '>=14.13.1'} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -5192,6 +5199,8 @@ snapshots: ts-debounce@4.0.0: {} + ts-deepmerge@7.0.3: {} + tslib@2.8.1: {} type-check@0.4.0: