forked from open-webui/open-webui
		
	Merge pull request #689 from ollama-webui/tts-playback
feat: tts automatic playback
This commit is contained in:
		
						commit
						cb5520c519
					
				
					 5 changed files with 100 additions and 47 deletions
				
			
		|  | @ -458,6 +458,7 @@ | ||||||
| 										</button> | 										</button> | ||||||
| 
 | 
 | ||||||
| 										<button | 										<button | ||||||
|  | 											id="speak-button-{message.id}" | ||||||
| 											class="{isLastMessage | 											class="{isLastMessage | ||||||
| 												? 'visible' | 												? 'visible' | ||||||
| 												: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white transition" | 												: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white transition" | ||||||
|  |  | ||||||
|  | @ -10,7 +10,6 @@ | ||||||
| 
 | 
 | ||||||
| 	// Addons | 	// Addons | ||||||
| 	let titleAutoGenerate = true; | 	let titleAutoGenerate = true; | ||||||
| 	let speechAutoSend = false; |  | ||||||
| 	let responseAutoCopy = false; | 	let responseAutoCopy = false; | ||||||
| 	let titleAutoGenerateModel = ''; | 	let titleAutoGenerateModel = ''; | ||||||
| 
 | 
 | ||||||
|  | @ -23,12 +22,6 @@ | ||||||
| 		saveSettings({ showUsername: showUsername }); | 		saveSettings({ showUsername: showUsername }); | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| 	const toggleSpeechAutoSend = async () => { |  | ||||||
| 		speechAutoSend = !speechAutoSend; |  | ||||||
| 		saveSettings({ speechAutoSend: speechAutoSend }); |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	const toggleTitleAutoGenerate = async () => { | 	const toggleTitleAutoGenerate = async () => { | ||||||
| 		titleAutoGenerate = !titleAutoGenerate; | 		titleAutoGenerate = !titleAutoGenerate; | ||||||
| 		saveSettings({ titleAutoGenerate: titleAutoGenerate }); | 		saveSettings({ titleAutoGenerate: titleAutoGenerate }); | ||||||
|  | @ -69,7 +62,6 @@ | ||||||
| 		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | 		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | ||||||
| 
 | 
 | ||||||
| 		titleAutoGenerate = settings.titleAutoGenerate ?? true; | 		titleAutoGenerate = settings.titleAutoGenerate ?? true; | ||||||
| 		speechAutoSend = settings.speechAutoSend ?? false; |  | ||||||
| 		responseAutoCopy = settings.responseAutoCopy ?? false; | 		responseAutoCopy = settings.responseAutoCopy ?? false; | ||||||
| 		showUsername = settings.showUsername ?? false; | 		showUsername = settings.showUsername ?? false; | ||||||
| 		titleAutoGenerateModel = settings.titleAutoGenerateModel ?? ''; | 		titleAutoGenerateModel = settings.titleAutoGenerateModel ?? ''; | ||||||
|  | @ -107,26 +99,6 @@ | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
| 
 | 
 | ||||||
| 			<div> |  | ||||||
| 				<div class=" py-0.5 flex w-full justify-between"> |  | ||||||
| 					<div class=" self-center text-xs font-medium">Voice Input Auto-Send</div> |  | ||||||
| 
 |  | ||||||
| 					<button |  | ||||||
| 						class="p-1 px-3 text-xs flex rounded transition" |  | ||||||
| 						on:click={() => { |  | ||||||
| 							toggleSpeechAutoSend(); |  | ||||||
| 						}} |  | ||||||
| 						type="button" |  | ||||||
| 					> |  | ||||||
| 						{#if speechAutoSend === true} |  | ||||||
| 							<span class="ml-2 self-center">On</span> |  | ||||||
| 						{:else} |  | ||||||
| 							<span class="ml-2 self-center">Off</span> |  | ||||||
| 						{/if} |  | ||||||
| 					</button> |  | ||||||
| 				</div> |  | ||||||
| 			</div> |  | ||||||
| 
 |  | ||||||
| 			<div> | 			<div> | ||||||
| 				<div class=" py-0.5 flex w-full justify-between"> | 				<div class=" py-0.5 flex w-full justify-between"> | ||||||
| 					<div class=" self-center text-xs font-medium">Response AutoCopy to Clipboard</div> | 					<div class=" self-center text-xs font-medium">Response AutoCopy to Clipboard</div> | ||||||
|  | @ -146,9 +118,12 @@ | ||||||
| 					</button> | 					</button> | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
|  | 
 | ||||||
| 			<div> | 			<div> | ||||||
| 				<div class=" py-0.5 flex w-full justify-between"> | 				<div class=" py-0.5 flex w-full justify-between"> | ||||||
| 					<div class=" self-center text-xs font-medium">Display the username instead of "You" in the Chat</div> | 					<div class=" self-center text-xs font-medium"> | ||||||
|  | 						Display the username instead of "You" in the Chat | ||||||
|  | 					</div> | ||||||
| 
 | 
 | ||||||
| 					<button | 					<button | ||||||
| 						class="p-1 px-3 text-xs flex rounded transition" | 						class="p-1 px-3 text-xs flex rounded transition" | ||||||
|  |  | ||||||
|  | @ -5,6 +5,10 @@ | ||||||
| 	export let saveSettings: Function; | 	export let saveSettings: Function; | ||||||
| 
 | 
 | ||||||
| 	// Voice | 	// Voice | ||||||
|  | 
 | ||||||
|  | 	let speechAutoSend = false; | ||||||
|  | 	let responseAutoPlayback = false; | ||||||
|  | 
 | ||||||
| 	let engines = ['', 'openai']; | 	let engines = ['', 'openai']; | ||||||
| 	let engine = ''; | 	let engine = ''; | ||||||
| 
 | 
 | ||||||
|  | @ -33,9 +37,22 @@ | ||||||
| 		}, 100); | 		}, 100); | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
|  | 	const toggleResponseAutoPlayback = async () => { | ||||||
|  | 		responseAutoPlayback = !responseAutoPlayback; | ||||||
|  | 		saveSettings({ responseAutoPlayback: responseAutoPlayback }); | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	const toggleSpeechAutoSend = async () => { | ||||||
|  | 		speechAutoSend = !speechAutoSend; | ||||||
|  | 		saveSettings({ speechAutoSend: speechAutoSend }); | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
| 	onMount(async () => { | 	onMount(async () => { | ||||||
| 		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | 		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | ||||||
| 
 | 
 | ||||||
|  | 		speechAutoSend = settings.speechAutoSend ?? false; | ||||||
|  | 		responseAutoPlayback = settings.responseAutoPlayback ?? false; | ||||||
|  | 
 | ||||||
| 		engine = settings?.speech?.engine ?? ''; | 		engine = settings?.speech?.engine ?? ''; | ||||||
| 		speaker = settings?.speech?.speaker ?? ''; | 		speaker = settings?.speech?.speaker ?? ''; | ||||||
| 
 | 
 | ||||||
|  | @ -60,26 +77,66 @@ | ||||||
| 	}} | 	}} | ||||||
| > | > | ||||||
| 	<div class=" space-y-3"> | 	<div class=" space-y-3"> | ||||||
| 		<div class=" py-0.5 flex w-full justify-between"> | 		<div> | ||||||
| 			<div class=" self-center text-sm font-medium">Speech Engine</div> | 			<div class=" mb-1 text-sm font-medium">TTS Settings</div> | ||||||
| 			<div class="flex items-center relative"> | 
 | ||||||
| 				<select | 			<div class=" py-0.5 flex w-full justify-between"> | ||||||
| 					class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right" | 				<div class=" self-center text-xs font-medium">Speech Engine</div> | ||||||
| 					bind:value={engine} | 				<div class="flex items-center relative"> | ||||||
| 					placeholder="Select a mode" | 					<select | ||||||
| 					on:change={(e) => { | 						class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" | ||||||
| 						if (e.target.value === 'openai') { | 						bind:value={engine} | ||||||
| 							getOpenAIVoices(); | 						placeholder="Select a mode" | ||||||
| 							speaker = 'alloy'; | 						on:change={(e) => { | ||||||
| 						} else { | 							if (e.target.value === 'openai') { | ||||||
| 							getWebAPIVoices(); | 								getOpenAIVoices(); | ||||||
| 							speaker = ''; | 								speaker = 'alloy'; | ||||||
| 						} | 							} else { | ||||||
|  | 								getWebAPIVoices(); | ||||||
|  | 								speaker = ''; | ||||||
|  | 							} | ||||||
|  | 						}} | ||||||
|  | 					> | ||||||
|  | 						<option value="">Default (Web API)</option> | ||||||
|  | 						<option value="openai">Open AI</option> | ||||||
|  | 					</select> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 
 | ||||||
|  | 			<div class=" py-0.5 flex w-full justify-between"> | ||||||
|  | 				<div class=" self-center text-xs font-medium">Voice Input Auto-Send</div> | ||||||
|  | 
 | ||||||
|  | 				<button | ||||||
|  | 					class="p-1 px-3 text-xs flex rounded transition" | ||||||
|  | 					on:click={() => { | ||||||
|  | 						toggleSpeechAutoSend(); | ||||||
| 					}} | 					}} | ||||||
|  | 					type="button" | ||||||
| 				> | 				> | ||||||
| 					<option value="">Default (Web API)</option> | 					{#if speechAutoSend === true} | ||||||
| 					<option value="openai">Open AI</option> | 						<span class="ml-2 self-center">On</span> | ||||||
| 				</select> | 					{:else} | ||||||
|  | 						<span class="ml-2 self-center">Off</span> | ||||||
|  | 					{/if} | ||||||
|  | 				</button> | ||||||
|  | 			</div> | ||||||
|  | 
 | ||||||
|  | 			<div class=" py-0.5 flex w-full justify-between"> | ||||||
|  | 				<div class=" self-center text-xs font-medium">TTS Automatic Playback</div> | ||||||
|  | 
 | ||||||
|  | 				<button | ||||||
|  | 					class="p-1 px-3 text-xs flex rounded transition" | ||||||
|  | 					on:click={() => { | ||||||
|  | 						toggleResponseAutoPlayback(); | ||||||
|  | 					}} | ||||||
|  | 					type="button" | ||||||
|  | 				> | ||||||
|  | 					{#if responseAutoPlayback === true} | ||||||
|  | 						<span class="ml-2 self-center">On</span> | ||||||
|  | 					{:else} | ||||||
|  | 						<span class="ml-2 self-center">Off</span> | ||||||
|  | 					{/if} | ||||||
|  | 				</button> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -448,6 +448,11 @@ | ||||||
| 									if ($settings.responseAutoCopy) { | 									if ($settings.responseAutoCopy) { | ||||||
| 										copyToClipboard(responseMessage.content); | 										copyToClipboard(responseMessage.content); | ||||||
| 									} | 									} | ||||||
|  | 
 | ||||||
|  | 									if ($settings.responseAutoPlayback) { | ||||||
|  | 										await tick(); | ||||||
|  | 										document.getElementById(`speak-button-${responseMessage.id}`)?.click(); | ||||||
|  | 									} | ||||||
| 								} | 								} | ||||||
| 							} | 							} | ||||||
| 						} | 						} | ||||||
|  | @ -633,6 +638,11 @@ | ||||||
| 					copyToClipboard(responseMessage.content); | 					copyToClipboard(responseMessage.content); | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
|  | 				if ($settings.responseAutoPlayback) { | ||||||
|  | 					await tick(); | ||||||
|  | 					document.getElementById(`speak-button-${responseMessage.id}`)?.click(); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
| 				if (autoScroll) { | 				if (autoScroll) { | ||||||
| 					window.scrollTo({ top: document.body.scrollHeight }); | 					window.scrollTo({ top: document.body.scrollHeight }); | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
|  | @ -462,6 +462,11 @@ | ||||||
| 									if ($settings.responseAutoCopy) { | 									if ($settings.responseAutoCopy) { | ||||||
| 										copyToClipboard(responseMessage.content); | 										copyToClipboard(responseMessage.content); | ||||||
| 									} | 									} | ||||||
|  | 
 | ||||||
|  | 									if ($settings.responseAutoPlayback) { | ||||||
|  | 										await tick(); | ||||||
|  | 										document.getElementById(`speak-button-${responseMessage.id}`)?.click(); | ||||||
|  | 									} | ||||||
| 								} | 								} | ||||||
| 							} | 							} | ||||||
| 						} | 						} | ||||||
|  | @ -647,6 +652,11 @@ | ||||||
| 					copyToClipboard(responseMessage.content); | 					copyToClipboard(responseMessage.content); | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
|  | 				if ($settings.responseAutoPlayback) { | ||||||
|  | 					await tick(); | ||||||
|  | 					document.getElementById(`speak-button-${responseMessage.id}`)?.click(); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
| 				if (autoScroll) { | 				if (autoScroll) { | ||||||
| 					window.scrollTo({ top: document.body.scrollHeight }); | 					window.scrollTo({ top: document.body.scrollHeight }); | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy Jaeryang Baek
						Timothy Jaeryang Baek