This commit is contained in:
Timothy Jaeryang Baek
2026-06-29 14:03:11 -05:00
parent caadfdec0b
commit 5134e5ecfc
2 changed files with 33 additions and 45 deletions

View File

@@ -2126,18 +2126,13 @@ async def export_knowledge_by_id(id: str, user=Depends(get_admin_user), db: Asyn
zip_buffer.seek(0) zip_buffer.seek(0)
# Sanitize knowledge name for filename # Sanitize knowledge name for filename
# ASCII-safe fallback for the basic filename parameter (latin-1 safe) safe_name = ''.join(c if c.isalnum() or c in ' -_' else '_' for c in knowledge.name)
safe_name = ''.join(c if c.isascii() and (c.isalnum() or c in ' -_') else '_' for c in knowledge.name)
zip_filename = f'{safe_name}.zip' zip_filename = f'{safe_name}.zip'
# Use RFC 5987 filename* for non-ASCII names so the browser gets the real name
quoted_name = quote(f'{knowledge.name}.zip')
content_disposition = f'attachment; filename="{zip_filename}"; filename*=UTF-8\'\'{quoted_name}'
return StreamingResponse( return StreamingResponse(
zip_buffer, zip_buffer,
media_type='application/zip', media_type='application/zip',
headers={'Content-Disposition': content_disposition}, headers={'Content-Disposition': f"attachment; filename*=UTF-8''{quote(zip_filename, safe='')}"},
) )

View File

@@ -14,6 +14,7 @@
import Spinner from '$lib/components/common/Spinner.svelte'; import Spinner from '$lib/components/common/Spinner.svelte';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte'; import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import TTSVoiceInput from '$lib/components/workspace/Models/TTSVoiceInput.svelte';
import { TTS_RESPONSE_SPLIT } from '$lib/types'; import { TTS_RESPONSE_SPLIT } from '$lib/types';
@@ -58,8 +59,18 @@
let STT_WHISPER_MODEL_LOADING = false; let STT_WHISPER_MODEL_LOADING = false;
type Voice = {
id: string;
name?: string;
description?: string;
meta?: {
description?: string;
};
};
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
let voices: SpeechSynthesisVoice[] = []; let voices: SpeechSynthesisVoice[] = [];
let providerVoices: Voice[] = [];
let models: Awaited<ReturnType<typeof _getModels>>['models'] = []; let models: Awaited<ReturnType<typeof _getModels>>['models'] = [];
const getModels = async () => { const getModels = async () => {
@@ -82,6 +93,8 @@
const getVoices = async () => { const getVoices = async () => {
if (TTS_ENGINE === '') { if (TTS_ENGINE === '') {
providerVoices = [];
const getVoicesLoop = setInterval(() => { const getVoicesLoop = setInterval(() => {
voices = speechSynthesis.getVoices(); voices = speechSynthesis.getVoices();
@@ -92,14 +105,18 @@
} }
}, 100); }, 100);
} else { } else {
voices = [];
const res = await _getVoices(localStorage.token).catch((e) => { const res = await _getVoices(localStorage.token).catch((e) => {
toast.error(`${e}`); toast.error(`${e}`);
}); });
if (res) { if (res) {
console.log(res); console.log(res);
voices = res.voices; providerVoices = res.voices ?? [];
voices.sort((a, b) => a.name.localeCompare(b.name, $i18n.resolvedLanguage)); providerVoices.sort((a, b) =>
(a.name ?? a.id).localeCompare(b.name ?? b.id, $i18n.resolvedLanguage)
);
} }
} }
}; };
@@ -679,18 +696,12 @@
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('TTS Voice')}</div> <div class=" mb-1.5 text-xs font-medium">{$i18n.t('TTS Voice')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <TTSVoiceInput
list="voice-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_VOICE} bind:value={TTS_VOICE}
voices={providerVoices}
placeholder={$i18n.t('Select a voice')} placeholder={$i18n.t('Select a voice')}
className="w-full rounded-lg py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850"
/> />
<datalist id="voice-list">
{#each voices as voice}
<option value={voice.id}>{voice.name}</option>
{/each}
</datalist>
</div> </div>
</div> </div>
</div> </div>
@@ -736,18 +747,12 @@
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('TTS Voice')}</div> <div class=" mb-1.5 text-xs font-medium">{$i18n.t('TTS Voice')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <TTSVoiceInput
list="voice-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_VOICE} bind:value={TTS_VOICE}
voices={providerVoices}
placeholder={$i18n.t('Select a voice')} placeholder={$i18n.t('Select a voice')}
className="w-full rounded-lg py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850"
/> />
<datalist id="voice-list">
{#each voices as voice}
<option value={voice.id}>{voice.name}</option>
{/each}
</datalist>
</div> </div>
</div> </div>
</div> </div>
@@ -777,18 +782,12 @@
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('TTS Voice')}</div> <div class=" mb-1.5 text-xs font-medium">{$i18n.t('TTS Voice')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <TTSVoiceInput
list="voice-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_VOICE} bind:value={TTS_VOICE}
voices={providerVoices}
placeholder={$i18n.t('Select a voice')} placeholder={$i18n.t('Select a voice')}
className="w-full rounded-lg py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850"
/> />
<datalist id="voice-list">
{#each voices as voice}
<option value={voice.id}>{voice.name}</option>
{/each}
</datalist>
</div> </div>
</div> </div>
</div> </div>
@@ -820,18 +819,12 @@
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('TTS Voice')}</div> <div class=" mb-1.5 text-xs font-medium">{$i18n.t('TTS Voice')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <TTSVoiceInput
list="voice-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_VOICE} bind:value={TTS_VOICE}
voices={providerVoices}
placeholder={$i18n.t('Select a voice')} placeholder={$i18n.t('Select a voice')}
className="w-full rounded-lg py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850"
/> />
<datalist id="voice-list">
{#each voices as voice}
<option value={voice.id}>{voice.name}</option>
{/each}
</datalist>
</div> </div>
</div> </div>
</div> </div>