feat(audio): cap voice recording duration

Add an optional admin-configured duration limit for voice input and automatically submit the captured recording when the limit is reached.

Refs #28715

Assisted-by: ChatGPT 5.2
This commit is contained in:
alexliluz 2026-08-19 20:37:29 +08:00
parent 5ea9ff3ed9
commit 85b61c650c
6 changed files with 35 additions and 0 deletions

View file

@ -1552,6 +1552,10 @@ AUDIO_STT_ENGINE = os.getenv('AUDIO_STT_ENGINE', '')
AUDIO_STT_MODEL = os.getenv('AUDIO_STT_MODEL', '')
AUDIO_STT_MAX_RECORDING_DURATION = (
int(os.getenv('AUDIO_STT_MAX_RECORDING_DURATION')) if os.getenv('AUDIO_STT_MAX_RECORDING_DURATION') else None
)
AUDIO_STT_SUPPORTED_CONTENT_TYPES = [
content_type.strip()
for content_type in os.getenv('AUDIO_STT_SUPPORTED_CONTENT_TYPES', '').split(',')
@ -3045,6 +3049,7 @@ DEFAULT_CONFIG = {
'audio.stt.openai.api_request_format': AUDIO_STT_OPENAI_API_REQUEST_FORMAT,
'audio.stt.engine': AUDIO_STT_ENGINE,
'audio.stt.model': AUDIO_STT_MODEL,
'audio.stt.max_recording_duration': AUDIO_STT_MAX_RECORDING_DURATION,
'audio.stt.supported_content_types': AUDIO_STT_SUPPORTED_CONTENT_TYPES,
'audio.stt.allowed_extensions': AUDIO_STT_ALLOWED_EXTENSIONS,
'audio.stt.azure.api_key': AUDIO_STT_AZURE_API_KEY,

View file

@ -2261,6 +2261,7 @@ async def get_app_config(request: Request):
'audio.tts.voice',
'audio.tts.split_on',
'audio.stt.engine',
'audio.stt.max_recording_duration',
'rag.file.max_size',
'rag.file.max_count',
'file.image_compression_width',
@ -2363,6 +2364,7 @@ async def get_app_config(request: Request):
},
'stt': {
'engine': config.get('audio.stt.engine'),
'max_recording_duration': config.get('audio.stt.max_recording_duration'),
},
},
'file': {

View file

@ -97,6 +97,7 @@ STT_CONFIG_KEYS = {
'OPENAI_API_REQUEST_FORMAT': 'audio.stt.openai.api_request_format',
'ENGINE': 'audio.stt.engine',
'MODEL': 'audio.stt.model',
'MAX_RECORDING_DURATION': 'audio.stt.max_recording_duration',
'SUPPORTED_CONTENT_TYPES': 'audio.stt.supported_content_types',
'ALLOWED_EXTENSIONS': 'audio.stt.allowed_extensions',
'WHISPER_MODEL': 'audio.stt.whisper_model',
@ -256,6 +257,7 @@ class STTConfigForm(BaseModel):
OPENAI_API_REQUEST_FORMAT: str = 'multipart'
ENGINE: str
MODEL: str
MAX_RECORDING_DURATION: Optional[int] = None
SUPPORTED_CONTENT_TYPES: list[str] = []
ALLOWED_EXTENSIONS: list[str] = []
WHISPER_MODEL: str

View file

@ -51,6 +51,7 @@
let STT_OPENAI_API_REQUEST_FORMAT = 'multipart';
let STT_ENGINE = '';
let STT_MODEL = '';
let STT_MAX_RECORDING_DURATION: number | '' = '';
let STT_SUPPORTED_CONTENT_TYPES = '';
let STT_WHISPER_MODEL = '';
let STT_AZURE_API_KEY = '';
@ -165,6 +166,9 @@
OPENAI_API_REQUEST_FORMAT: STT_OPENAI_API_REQUEST_FORMAT,
ENGINE: STT_ENGINE,
MODEL: STT_MODEL,
MAX_RECORDING_DURATION: STT_MAX_RECORDING_DURATION
? Number(STT_MAX_RECORDING_DURATION)
: null,
SUPPORTED_CONTENT_TYPES: STT_SUPPORTED_CONTENT_TYPES.split(','),
WHISPER_MODEL: STT_WHISPER_MODEL,
DEEPGRAM_API_KEY: STT_DEEPGRAM_API_KEY,
@ -219,6 +223,7 @@
STT_ENGINE = res.stt.ENGINE;
STT_MODEL = res.stt.MODEL;
STT_MAX_RECORDING_DURATION = res.stt.MAX_RECORDING_DURATION ?? '';
STT_SUPPORTED_CONTENT_TYPES = (res?.stt?.SUPPORTED_CONTENT_TYPES ?? []).join(',');
STT_WHISPER_MODEL = res.stt.WHISPER_MODEL;
STT_AZURE_API_KEY = res.stt.AZURE_API_KEY;
@ -262,6 +267,22 @@
</SettingsSelect>
</AdminSettingRow>
<AdminSettingField
label={$i18n.t('Maximum Recording Duration (seconds)')}
description={$i18n.t(
'Automatically stop and submit voice recordings after this many seconds. Leave blank for no limit.'
)}
>
<input
class={inputClass}
type="number"
min="1"
step="1"
bind:value={STT_MAX_RECORDING_DURATION}
placeholder={$i18n.t('No limit')}
/>
</AdminSettingField>
{#if STT_ENGINE !== 'web'}
<AdminSettingField
label={$i18n.t('Supported MIME Types')}

View file

@ -1597,6 +1597,7 @@
<div class={recording ? '' : 'hidden'}>
<VoiceRecording
bind:recording
maxDurationSeconds={$config?.audio?.stt?.max_recording_duration ?? 0}
onCancel={async () => {
recording = false;

View file

@ -20,6 +20,7 @@
export let echoCancellation = true;
export let noiseSuppression = true;
export let autoGainControl = true;
export let maxDurationSeconds = 0;
export let className = ' p-2.5 w-full max-w-full';
@ -37,6 +38,9 @@
const startDurationCounter = () => {
durationCounter = setInterval(() => {
durationSeconds++;
if (maxDurationSeconds > 0 && durationSeconds >= maxDurationSeconds && recording && !loading) {
void confirmRecording();
}
}, 1000);
};