From b151321635a933c4b1cf58d788609c1260655e14 Mon Sep 17 00:00:00 2001 From: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> Date: Thu, 20 Aug 2026 00:10:08 +0000 Subject: [PATCH 1/2] feat(ui): add per-request TTS instructions Let studio users guide speech delivery for backends that support request instructions. Blank guidance stays out of requests and media history. Assisted-by: Codex:gpt-5 --- core/http/react-ui/e2e/voice-library.spec.js | 26 +++++++++++++++++++ .../react-ui/public/locales/de/media.json | 5 +++- .../react-ui/public/locales/en/media.json | 5 +++- .../react-ui/public/locales/es/media.json | 5 +++- .../react-ui/public/locales/id/media.json | 5 +++- .../react-ui/public/locales/it/media.json | 5 +++- .../react-ui/public/locales/ko/media.json | 5 +++- .../react-ui/public/locales/pt-BR/media.json | 5 +++- .../react-ui/public/locales/zh-CN/media.json | 5 +++- core/http/react-ui/src/pages/TTS.jsx | 23 +++++++++++++--- docs/content/features/text-to-audio.md | 17 ++++++++++++ 11 files changed, 95 insertions(+), 11 deletions(-) diff --git a/core/http/react-ui/e2e/voice-library.spec.js b/core/http/react-ui/e2e/voice-library.spec.js index 07b82ccb4920..2a471433e876 100644 --- a/core/http/react-ui/e2e/voice-library.spec.js +++ b/core/http/react-ui/e2e/voice-library.spec.js @@ -152,6 +152,32 @@ test.describe('Voice Library', () => { expect(ttsBody.model).toBe('qwen-base') }) + test('sends trimmed speech instructions and omits blank instructions', async ({ page }) => { + const ttsBodies = [] + await page.route('**/tts', async route => { + ttsBodies.push(route.request().postDataJSON()) + await route.fulfill({ + status: 200, + contentType: 'audio/wav', + headers: { 'Content-Disposition': 'attachment; filename="speech.wav"' }, + body: pcmWav(1), + }) + }) + + await page.goto('/app/tts') + await page.getByPlaceholder('Enter text to synthesize...').fill('Read this sentence.') + await page.getByLabel('Instructions').fill(' Speak slowly and warmly. ') + await page.getByRole('button', { name: /Generate$/ }).click() + await expect.poll(() => ttsBodies.length).toBe(1) + expect(ttsBodies[0].instructions).toBe('Speak slowly and warmly.') + + await expect(page.getByRole('button', { name: /Generate$/ })).toBeEnabled() + await page.getByLabel('Instructions').fill(' \n ') + await page.getByRole('button', { name: /Generate$/ }).click() + await expect.poll(() => ttsBodies.length).toBe(2) + expect(ttsBodies[1]).not.toHaveProperty('instructions') + }) + test('normalizes an upload and creates a consented profile', async ({ page }) => { await page.goto('/app/voice-library/new') await page.locator('#voice-profile-audio-file').setInputFiles({ diff --git a/core/http/react-ui/public/locales/de/media.json b/core/http/react-ui/public/locales/de/media.json index d74205e68ed3..45d63d40ef5e 100644 --- a/core/http/react-ui/public/locales/de/media.json +++ b/core/http/react-ui/public/locales/de/media.json @@ -103,7 +103,10 @@ "voice": "Stimme", "voicePlaceholder": "Optionale Stimm-ID", "input": "Text", - "inputPlaceholder": "Text zum Synthetisieren eingeben..." + "inputPlaceholder": "Text zum Synthetisieren eingeben...", + "instructions": "Anweisungen", + "instructionsPlaceholder": "Zum Beispiel: Langsam und warmherzig sprechen", + "instructionsHint": "Optionale Hinweise zur Sprechweise. Die Unterstützung hängt vom Backend ab." }, "actions": { "generate": "Audio generieren", diff --git a/core/http/react-ui/public/locales/en/media.json b/core/http/react-ui/public/locales/en/media.json index 1b392d9d5c7b..d0cb518f2c76 100644 --- a/core/http/react-ui/public/locales/en/media.json +++ b/core/http/react-ui/public/locales/en/media.json @@ -157,7 +157,10 @@ "voice": "Voice", "voicePlaceholder": "Optional speaker or voice ID", "input": "Text", - "inputPlaceholder": "Enter text to synthesize..." + "inputPlaceholder": "Enter text to synthesize...", + "instructions": "Instructions", + "instructionsPlaceholder": "For example: Speak slowly and warmly", + "instructionsHint": "Optional delivery guidance. Support depends on the backend." }, "actions": { "generate": "Generate", diff --git a/core/http/react-ui/public/locales/es/media.json b/core/http/react-ui/public/locales/es/media.json index 9680f5729d83..fb5796b20b3b 100644 --- a/core/http/react-ui/public/locales/es/media.json +++ b/core/http/react-ui/public/locales/es/media.json @@ -103,7 +103,10 @@ "voice": "Voz", "voicePlaceholder": "ID de voz opcional", "input": "Texto", - "inputPlaceholder": "Introduce el texto a sintetizar..." + "inputPlaceholder": "Introduce el texto a sintetizar...", + "instructions": "Instrucciones", + "instructionsPlaceholder": "Por ejemplo: Habla despacio y con calidez", + "instructionsHint": "Indicaciones opcionales sobre la locución. La compatibilidad depende del backend." }, "actions": { "generate": "Generar audio", diff --git a/core/http/react-ui/public/locales/id/media.json b/core/http/react-ui/public/locales/id/media.json index 9eb6da5fcac6..72512a338cb4 100644 --- a/core/http/react-ui/public/locales/id/media.json +++ b/core/http/react-ui/public/locales/id/media.json @@ -103,7 +103,10 @@ "voice": "Suara", "voicePlaceholder": "ID suara opsional", "input": "Teks", - "inputPlaceholder": "Masukkan teks untuk disintesis..." + "inputPlaceholder": "Masukkan teks untuk disintesis...", + "instructions": "Instruksi", + "instructionsPlaceholder": "Contoh: Ucapkan perlahan dan hangat", + "instructionsHint": "Panduan penyampaian opsional. Dukungan bergantung pada backend." }, "actions": { "generate": "Hasilkan", diff --git a/core/http/react-ui/public/locales/it/media.json b/core/http/react-ui/public/locales/it/media.json index 2dd8be9848b9..efd019e0fc00 100644 --- a/core/http/react-ui/public/locales/it/media.json +++ b/core/http/react-ui/public/locales/it/media.json @@ -103,7 +103,10 @@ "voice": "Voce", "voicePlaceholder": "ID voce opzionale", "input": "Testo", - "inputPlaceholder": "Inserisci il testo da convertire in voce..." + "inputPlaceholder": "Inserisci il testo da convertire in voce...", + "instructions": "Istruzioni", + "instructionsPlaceholder": "Ad esempio: Parla lentamente e con calore", + "instructionsHint": "Indicazioni facoltative sullo stile. Il supporto dipende dal backend." }, "actions": { "generate": "Genera audio", diff --git a/core/http/react-ui/public/locales/ko/media.json b/core/http/react-ui/public/locales/ko/media.json index 30f8fd1c12df..52e7ebd864d0 100644 --- a/core/http/react-ui/public/locales/ko/media.json +++ b/core/http/react-ui/public/locales/ko/media.json @@ -103,7 +103,10 @@ "voice": "음성", "voicePlaceholder": "음성 ID (선택 사항)", "input": "텍스트", - "inputPlaceholder": "합성할 텍스트를 입력하세요..." + "inputPlaceholder": "합성할 텍스트를 입력하세요...", + "instructions": "지침", + "instructionsPlaceholder": "예: 천천히 따뜻한 어조로 말하기", + "instructionsHint": "선택 사항인 말하기 지침입니다. 지원 여부는 백엔드에 따라 다릅니다." }, "actions": { "generate": "생성", diff --git a/core/http/react-ui/public/locales/pt-BR/media.json b/core/http/react-ui/public/locales/pt-BR/media.json index 2648d4665158..8d9e1105519b 100644 --- a/core/http/react-ui/public/locales/pt-BR/media.json +++ b/core/http/react-ui/public/locales/pt-BR/media.json @@ -157,7 +157,10 @@ "voice": "Voz", "voicePlaceholder": "Falante opcional ou ID de voz", "input": "Texto", - "inputPlaceholder": "Digite o texto para sintetizar..." + "inputPlaceholder": "Digite o texto para sintetizar...", + "instructions": "Instruções", + "instructionsPlaceholder": "Por exemplo: Fale devagar e com calor", + "instructionsHint": "Orientações opcionais de interpretação. O suporte depende do backend." }, "actions": { "generate": "Gerar", diff --git a/core/http/react-ui/public/locales/zh-CN/media.json b/core/http/react-ui/public/locales/zh-CN/media.json index 6c1dac33124d..9af8856b722b 100644 --- a/core/http/react-ui/public/locales/zh-CN/media.json +++ b/core/http/react-ui/public/locales/zh-CN/media.json @@ -103,7 +103,10 @@ "voice": "声音", "voicePlaceholder": "可选的声音 ID", "input": "文本", - "inputPlaceholder": "输入要合成的文本..." + "inputPlaceholder": "输入要合成的文本...", + "instructions": "朗读指令", + "instructionsPlaceholder": "例如:缓慢、温暖地朗读", + "instructionsHint": "可选的表达方式指导。是否支持取决于后端。" }, "actions": { "generate": "生成音频", diff --git a/core/http/react-ui/src/pages/TTS.jsx b/core/http/react-ui/src/pages/TTS.jsx index eaf124584186..6137da625d7d 100644 --- a/core/http/react-ui/src/pages/TTS.jsx +++ b/core/http/react-ui/src/pages/TTS.jsx @@ -32,6 +32,7 @@ export default function TTS() { const [manualVoice, setManualVoice] = useState('') const [voiceProfileID, setVoiceProfileID] = useState(requestedVoiceID) const [text, setText] = useState('') + const [instructions, setInstructions] = useState('') const [loading, setLoading] = useState(false) const [error, setError] = useState(null) // What was actually sent, so the request panel records rather than predicts. @@ -75,6 +76,7 @@ export default function TTS() { const selectedVoice = supportsVoiceProfiles ? selectedProfile?.voice : manualVoice.trim() const request = { model, input: text.trim() } if (selectedVoice) request.voice = selectedVoice + if (instructions.trim()) request.instructions = instructions.trim() setLastRequest(request) const { blob, serverUrl } = await ttsApi.generate(request) const url = URL.createObjectURL(blob) @@ -84,9 +86,12 @@ export default function TTS() { addEntry({ prompt: text.trim(), model, - params: selectedProfile - ? { voice: selectedProfile.name } - : (!supportsVoiceProfiles && manualVoice.trim() ? { voice: manualVoice.trim() } : {}), + params: { + ...(selectedProfile + ? { voice: selectedProfile.name } + : (!supportsVoiceProfiles && manualVoice.trim() ? { voice: manualVoice.trim() } : {})), + ...(instructions.trim() ? { instructions: instructions.trim() } : {}), + }, results: [{ url: serverUrl }], }) } @@ -168,6 +173,18 @@ export default function TTS() { rows={5} /> +
+ +