mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Speex: TTS character limit settings. Fixes #942
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { SpeexConfigureEngines } from '~/modules/speex/components/SpeexConfigureEngines';
|
||||
import { useSpeexEngines } from '~/modules/speex/store-module-speex';
|
||||
import { useSpeexEngines, useSpeexTtsCharLimit } from '~/modules/speex/store-module-speex';
|
||||
|
||||
import { ChatAutoSpeakType, useChatAutoAI } from '../chat/store-app-chat';
|
||||
|
||||
import { FormRadioOption } from '~/common/components/forms/FormRadioControl';
|
||||
import { FormChipControl } from '~/common/components/forms/FormChipControl';
|
||||
import { FormRadioOption } from '~/common/components/forms/FormRadioControl';
|
||||
import { FormSwitchControl } from '~/common/components/forms/FormSwitchControl';
|
||||
|
||||
|
||||
const _autoSpeakOptions: FormRadioOption<ChatAutoSpeakType>[] = [
|
||||
@@ -21,6 +22,7 @@ export function VoiceOutSettings(props: { isMobile: boolean }) {
|
||||
|
||||
// external state
|
||||
const { autoSpeak, setAutoSpeak } = useChatAutoAI();
|
||||
const { ttsCharLimit, setTtsCharLimit } = useSpeexTtsCharLimit();
|
||||
|
||||
// external state - module
|
||||
const hasEngines = useSpeexEngines().length > 0;
|
||||
@@ -39,6 +41,15 @@ export function VoiceOutSettings(props: { isMobile: boolean }) {
|
||||
onChange={setAutoSpeak}
|
||||
/>
|
||||
|
||||
{/* TTS character limit toggle */}
|
||||
<FormSwitchControl
|
||||
title='Speak Cost Guard'
|
||||
description={ttsCharLimit !== null ? 'Max ~3 min' : 'Unlimited'}
|
||||
tooltip='Limits text sent to TTS providers, helping prevent unexpected costs with cloud services'
|
||||
checked={ttsCharLimit !== null}
|
||||
onChange={(checked) => setTtsCharLimit(checked ? 4096 : null)}
|
||||
/>
|
||||
|
||||
{/* Engine configuration */}
|
||||
<SpeexConfigureEngines isMobile={props.isMobile} />
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { returnAudioWholeOrThrow, streamAudioChunksOrThrow } from './rpc.streami
|
||||
|
||||
|
||||
// configuration
|
||||
const SAFETY_TEXT_LENGTH = 1000;
|
||||
const SAFETY_TEXT_LENGTH = 40000; // fallback safety net (user limit applied in speex.client.ts)
|
||||
const MIN_CHUNK_SIZE = 4096;
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { returnAudioWholeOrThrow, streamAudioChunksOrThrow } from './rpc.streami
|
||||
|
||||
|
||||
// configuration
|
||||
const SAFETY_TEXT_LENGTH = 4096; // OpenAI max
|
||||
const SAFETY_TEXT_LENGTH = 40000; // fallback safety net (user limit applied in speex.client.ts)
|
||||
const MIN_CHUNK_SIZE = 4096; // bytes
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { useUIPreferencesStore } from '~/common/stores/store-ui';
|
||||
|
||||
import type { DSpeexEngineAny, DVoiceWebSpeech, SpeexSpeakOptions, SpeexSpeakResult, SpeexVoiceSelector } from './speex.types';
|
||||
import { speexFindEngineById, speexFindGlobalEngine, speexFindValidEngineByType } from './store-module-speex';
|
||||
import { speexFindEngineById, speexFindGlobalEngine, speexFindValidEngineByType, speexGetTtsCharLimit } from './store-module-speex';
|
||||
|
||||
import { speexSynthesize_RPC } from './protocols/rpc/rpc.client';
|
||||
import { speexSynthesize_WebSpeech } from './protocols/webspeech/webspeech.client';
|
||||
@@ -43,17 +43,24 @@ export async function speakText(
|
||||
// apply voice override from selector (merge with engine defaults)
|
||||
const effectiveEngine = _engineApplyVoiceOverride(engine, voiceSelector);
|
||||
|
||||
// apply user-configurable character limit (null = unlimited)
|
||||
const charLimit = speexGetTtsCharLimit();
|
||||
const truncated = charLimit !== null && inputText.length > charLimit;
|
||||
const text = truncated ? inputText.slice(0, charLimit) : inputText;
|
||||
if (truncated)
|
||||
console.log(`[Speex] Text truncated from ${inputText.length} to ${charLimit} characters`);
|
||||
|
||||
try {
|
||||
switch (effectiveEngine.vendorType) {
|
||||
// RPC providers: route through speex.router RPC
|
||||
case 'elevenlabs':
|
||||
case 'openai':
|
||||
case 'localai':
|
||||
return speexSynthesize_RPC(effectiveEngine, inputText, { streaming, playback, returnAudio, languageCode, priority }, callbacks);
|
||||
return speexSynthesize_RPC(effectiveEngine, text, { streaming, playback, returnAudio, languageCode, priority }, callbacks);
|
||||
|
||||
// Web Speech: client-only, no RPC
|
||||
case 'webspeech':
|
||||
return speexSynthesize_WebSpeech(inputText, effectiveEngine.voice as DVoiceWebSpeech, callbacks);
|
||||
return speexSynthesize_WebSpeech(text, effectiveEngine.voice as DVoiceWebSpeech, callbacks);
|
||||
}
|
||||
} catch (error) {
|
||||
callbacks?.onError?.(error instanceof Error ? error : new Error(String(error)));
|
||||
|
||||
@@ -19,6 +19,9 @@ interface SpeexStoreState {
|
||||
engines: Record<SpeexEngineId, DSpeexEngineAny>;
|
||||
activeEngineId: SpeexEngineId | null; // null = no user selection = use global auto-selection
|
||||
|
||||
// TTS character limit: number = limit chars, null = unlimited (default: 4096)
|
||||
ttsCharLimit: number | null;
|
||||
|
||||
// to avoid repeated migrations
|
||||
hasInitializedLlms: boolean;
|
||||
hasMigratedElevenLabs: boolean;
|
||||
@@ -35,6 +38,9 @@ interface SpeexStoreActions {
|
||||
// selection
|
||||
setActiveEngineId: (engineId: SpeexEngineId | null) => void;
|
||||
|
||||
// TTS settings
|
||||
setTtsCharLimit: (limit: number | null) => void;
|
||||
|
||||
// business logic: auto-detection or migration
|
||||
syncWebSpeechEngine: () => boolean;
|
||||
syncEnginesFromLLMServices: (llmsSources: ReturnType<typeof useModelsStore.getState>['sources']) => boolean;
|
||||
@@ -51,6 +57,7 @@ export const useSpeexStore = create<SpeexStore>()(persist(
|
||||
// initial state
|
||||
engines: {},
|
||||
activeEngineId: null,
|
||||
ttsCharLimit: 4096, // default: ~3 min of speech, null = unlimited
|
||||
hasInitializedLlms: false,
|
||||
hasMigratedElevenLabs: false,
|
||||
|
||||
@@ -129,6 +136,13 @@ export const useSpeexStore = create<SpeexStore>()(persist(
|
||||
},
|
||||
|
||||
|
||||
// TTS settings
|
||||
|
||||
setTtsCharLimit: (limit) => {
|
||||
set({ ttsCharLimit: limit });
|
||||
},
|
||||
|
||||
|
||||
// Auto-detections
|
||||
|
||||
syncWebSpeechEngine: () => {
|
||||
@@ -378,6 +392,20 @@ export function speexFindGlobalEngine({ engines, activeEngineId }: SpeexStore =
|
||||
}
|
||||
|
||||
|
||||
// TTS character limit
|
||||
|
||||
export function useSpeexTtsCharLimit() {
|
||||
return useSpeexStore(useShallow(state => ({
|
||||
ttsCharLimit: state.ttsCharLimit,
|
||||
setTtsCharLimit: state.setTtsCharLimit,
|
||||
})));
|
||||
}
|
||||
|
||||
export function speexGetTtsCharLimit(): number | null {
|
||||
return useSpeexStore.getState().ttsCharLimit;
|
||||
}
|
||||
|
||||
|
||||
export function speexAreCredentialsValid(credentials: DSpeexCredentialsAny): boolean {
|
||||
switch (credentials.type) {
|
||||
case 'api-key':
|
||||
|
||||
Reference in New Issue
Block a user