Audio.svelte 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. <script lang="ts">
  2. import { createEventDispatcher, onMount, getContext } from 'svelte';
  3. import { toast } from 'svelte-sonner';
  4. const dispatch = createEventDispatcher();
  5. const i18n = getContext('i18n');
  6. export let saveSettings: Function;
  7. // Audio
  8. let STTEngines = ['', 'openai'];
  9. let STTEngine = '';
  10. let conversationMode = false;
  11. let speechAutoSend = false;
  12. let responseAutoPlayback = false;
  13. let TTSEngines = ['', 'openai'];
  14. let TTSEngine = '';
  15. let voices = [];
  16. let speaker = '';
  17. const getOpenAIVoices = () => {
  18. voices = [
  19. { name: 'alloy' },
  20. { name: 'echo' },
  21. { name: 'fable' },
  22. { name: 'onyx' },
  23. { name: 'nova' },
  24. { name: 'shimmer' }
  25. ];
  26. };
  27. const getWebAPIVoices = () => {
  28. const getVoicesLoop = setInterval(async () => {
  29. voices = await speechSynthesis.getVoices();
  30. // do your loop
  31. if (voices.length > 0) {
  32. clearInterval(getVoicesLoop);
  33. }
  34. }, 100);
  35. };
  36. const toggleConversationMode = async () => {
  37. conversationMode = !conversationMode;
  38. if (conversationMode) {
  39. responseAutoPlayback = true;
  40. speechAutoSend = true;
  41. }
  42. saveSettings({
  43. conversationMode: conversationMode,
  44. responseAutoPlayback: responseAutoPlayback,
  45. speechAutoSend: speechAutoSend
  46. });
  47. };
  48. const toggleResponseAutoPlayback = async () => {
  49. responseAutoPlayback = !responseAutoPlayback;
  50. saveSettings({ responseAutoPlayback: responseAutoPlayback });
  51. };
  52. const toggleSpeechAutoSend = async () => {
  53. speechAutoSend = !speechAutoSend;
  54. saveSettings({ speechAutoSend: speechAutoSend });
  55. };
  56. onMount(async () => {
  57. let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
  58. conversationMode = settings.conversationMode ?? false;
  59. speechAutoSend = settings.speechAutoSend ?? false;
  60. responseAutoPlayback = settings.responseAutoPlayback ?? false;
  61. STTEngine = settings?.audio?.STTEngine ?? '';
  62. TTSEngine = settings?.audio?.TTSEngine ?? '';
  63. speaker = settings?.audio?.speaker ?? '';
  64. if (TTSEngine === 'openai') {
  65. getOpenAIVoices();
  66. } else {
  67. getWebAPIVoices();
  68. }
  69. });
  70. </script>
  71. <form
  72. class="flex flex-col h-full justify-between space-y-3 text-sm"
  73. on:submit|preventDefault={() => {
  74. saveSettings({
  75. audio: {
  76. STTEngine: STTEngine !== '' ? STTEngine : undefined,
  77. TTSEngine: TTSEngine !== '' ? TTSEngine : undefined,
  78. speaker: speaker !== '' ? speaker : undefined
  79. }
  80. });
  81. dispatch('save');
  82. }}
  83. >
  84. <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
  85. <div>
  86. <div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
  87. <div class=" py-0.5 flex w-full justify-between">
  88. <div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
  89. <div class="flex items-center relative">
  90. <select
  91. class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
  92. bind:value={STTEngine}
  93. placeholder="Select a mode"
  94. on:change={(e) => {
  95. if (e.target.value !== '') {
  96. navigator.mediaDevices.getUserMedia({ audio: true }).catch(function (err) {
  97. toast.error(
  98. $i18n.t(`Permission denied when accessing microphone: {{error}}`, {
  99. error: err
  100. })
  101. );
  102. STTEngine = '';
  103. });
  104. }
  105. }}
  106. >
  107. <option value="">{$i18n.t('Default (Web API)')}</option>
  108. <option value="whisper-local">{$i18n.t('Whisper (Local)')}</option>
  109. </select>
  110. </div>
  111. </div>
  112. <div class=" py-0.5 flex w-full justify-between">
  113. <div class=" self-center text-xs font-medium">{$i18n.t('Conversation Mode')}</div>
  114. <button
  115. class="p-1 px-3 text-xs flex rounded transition"
  116. on:click={() => {
  117. toggleConversationMode();
  118. }}
  119. type="button"
  120. >
  121. {#if conversationMode === true}
  122. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  123. {:else}
  124. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  125. {/if}
  126. </button>
  127. </div>
  128. <div class=" py-0.5 flex w-full justify-between">
  129. <div class=" self-center text-xs font-medium">
  130. {$i18n.t('Auto-send input after 3 sec.')}
  131. </div>
  132. <button
  133. class="p-1 px-3 text-xs flex rounded transition"
  134. on:click={() => {
  135. toggleSpeechAutoSend();
  136. }}
  137. type="button"
  138. >
  139. {#if speechAutoSend === true}
  140. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  141. {:else}
  142. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  143. {/if}
  144. </button>
  145. </div>
  146. </div>
  147. <div>
  148. <div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div>
  149. <div class=" py-0.5 flex w-full justify-between">
  150. <div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
  151. <div class="flex items-center relative">
  152. <select
  153. class=" dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
  154. bind:value={TTSEngine}
  155. placeholder="Select a mode"
  156. on:change={(e) => {
  157. if (e.target.value === 'openai') {
  158. getOpenAIVoices();
  159. speaker = 'alloy';
  160. } else {
  161. getWebAPIVoices();
  162. speaker = '';
  163. }
  164. }}
  165. >
  166. <option value="">{$i18n.t('Default (Web API)')}</option>
  167. <option value="openai">{$i18n.t('Open AI')}</option>
  168. </select>
  169. </div>
  170. </div>
  171. <div class=" py-0.5 flex w-full justify-between">
  172. <div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div>
  173. <button
  174. class="p-1 px-3 text-xs flex rounded transition"
  175. on:click={() => {
  176. toggleResponseAutoPlayback();
  177. }}
  178. type="button"
  179. >
  180. {#if responseAutoPlayback === true}
  181. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  182. {:else}
  183. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  184. {/if}
  185. </button>
  186. </div>
  187. </div>
  188. <hr class=" dark:border-gray-700" />
  189. {#if TTSEngine === ''}
  190. <div>
  191. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
  192. <div class="flex w-full">
  193. <div class="flex-1">
  194. <select
  195. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  196. bind:value={speaker}
  197. placeholder="Select a voice"
  198. >
  199. <option value="" selected>{$i18n.t('Default')}</option>
  200. {#each voices.filter((v) => v.localService === true) as voice}
  201. <option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option
  202. >
  203. {/each}
  204. </select>
  205. </div>
  206. </div>
  207. </div>
  208. {:else if TTSEngine === 'openai'}
  209. <div>
  210. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
  211. <div class="flex w-full">
  212. <div class="flex-1">
  213. <select
  214. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  215. bind:value={speaker}
  216. placeholder="Select a voice"
  217. >
  218. {#each voices as voice}
  219. <option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option
  220. >
  221. {/each}
  222. </select>
  223. </div>
  224. </div>
  225. </div>
  226. {/if}
  227. </div>
  228. <div class="flex justify-end pt-3 text-sm font-medium">
  229. <button
  230. class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
  231. type="submit"
  232. >
  233. {$i18n.t('Save')}
  234. </button>
  235. </div>
  236. </form>