Merge pull request #2654 from ayancey/openai-compatible-tts OpenAI Compatible API TTS Provider
Signed| @@ -30,7 +30,7 @@ import { SlashCommand } from '../../slash-commands/SlashCommand.js'; | |||
| 30 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; | 30 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
| 31 | import { debounce_timeout } from '../../constants.js'; | 31 | import { debounce_timeout } from '../../constants.js'; |
| 32 | import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; | 32 | import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; |
| 33 | import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js'; | 33 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js'; |
| 34 | export { MODULE_NAME }; | 34 | export { MODULE_NAME }; |
| 35 | 35 | ||
| 36 | const MODULE_NAME = 'sd'; | 36 | const MODULE_NAME = 'sd'; |
| @@ -1097,7 +1097,18 @@ function onComfyWorkflowChange() { | |||
| 1097 | 1097 | ||
| 1098 | async function onStabilityKeyClick() { | 1098 | async function onStabilityKeyClick() { |
| 1099 | const popupText = 'Stability AI API Key:'; | 1099 | const popupText = 'Stability AI API Key:'; |
| 1100 | const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT); | 1100 | const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, '', { |
| 1101 | customButtons: [{ | ||
| 1102 | text: 'Remove Key', | ||
| 1103 | appendAtEnd: true, | ||
| 1104 | result: POPUP_RESULT.NEGATIVE, | ||
| 1105 | action: async () => { | ||
| 1106 | await writeSecret(SECRET_KEYS.STABILITY, ''); | ||
| 1107 | toastr.success('API Key removed'); | ||
| 1108 | await loadSettingOptions(); | ||
| 1109 | }, | ||
| 1110 | }], | ||
| 1111 | }); | ||
| 1101 | 1112 | ||
| 1102 | if (!key) { | 1113 | if (!key) { |
| 1103 | return; | 1114 | return; |
| @@ -2693,15 +2704,15 @@ async function generateBlockEntropyImage(prompt, negativePrompt, signal) { | |||
| 2693 | 2704 | ||
| 2694 | if (result.ok) { | 2705 | if (result.ok) { |
| 2695 | const data = await result.json(); | 2706 | const data = await result.json(); |
| 2696 | 2707 | ||
| 2697 | // Default format is 'jpg' | 2708 | // Default format is 'jpg' |
| 2698 | let format = 'jpg'; | 2709 | let format = 'jpg'; |
| 2699 | 2710 | ||
| 2700 | // Check if a format is specified in the result | 2711 | // Check if a format is specified in the result |
| 2701 | if (data.format) { | 2712 | if (data.format) { |
| 2702 | format = data.format.toLowerCase(); | 2713 | format = data.format.toLowerCase(); |
| 2703 | } | 2714 | } |
| 2704 | 2715 | ||
| 2705 | return { format: format, data: data.images[0] }; | 2716 | return { format: format, data: data.images[0] }; |
| 2706 | } else { | 2717 | } else { |
| 2707 | const text = await result.text(); | 2718 | const text = await result.text(); |
| @@ -10,7 +10,7 @@ import { | |||
| 10 | updateMessageBlock, | 10 | updateMessageBlock, |
| 11 | } from '../../../script.js'; | 11 | } from '../../../script.js'; |
| 12 | import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js'; | 12 | import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js'; |
| 13 | import { POPUP_TYPE, callGenericPopup } from '../../popup.js'; | 13 | import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from '../../popup.js'; |
| 14 | import { findSecret, secret_state, writeSecret } from '../../secrets.js'; | 14 | import { findSecret, secret_state, writeSecret } from '../../secrets.js'; |
| 15 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; | 15 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| 16 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; | 16 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
| @@ -621,7 +621,18 @@ jQuery(async () => { | |||
| 621 | const secretKey = extension_settings.translate.provider + '_url'; | 621 | const secretKey = extension_settings.translate.provider + '_url'; |
| 622 | const savedUrl = secret_state[secretKey] ? await findSecret(secretKey) : ''; | 622 | const savedUrl = secret_state[secretKey] ? await findSecret(secretKey) : ''; |
| 623 | 623 | ||
| 624 | const url = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedUrl); | 624 | const url = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedUrl,{ |
| 625 | customButtons: [{ | ||
| 626 | text: 'Remove URL', | ||
| 627 | appendAtEnd: true, | ||
| 628 | result: POPUP_RESULT.NEGATIVE, | ||
| 629 | action: async () => { | ||
| 630 | await writeSecret(secretKey, ''); | ||
| 631 | toastr.success('API URL removed'); | ||
| 632 | $('#translate_url_button').toggleClass('success', !!secret_state[secretKey]); | ||
| 633 | }, | ||
| 634 | }], | ||
| 635 | }); | ||
| 625 | 636 | ||
| 626 | if (url == false || url == '') { | 637 | if (url == false || url == '') { |
| 627 | return; | 638 | return; |
| @@ -1,5 +1,5 @@ | |||
| 1 | import { getRequestHeaders } from '../../../script.js'; | 1 | import { getRequestHeaders } from '../../../script.js'; |
| 2 | import { POPUP_TYPE, callGenericPopup } from '../../popup.js'; | 2 | import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from '../../popup.js'; |
| 3 | import { SECRET_KEYS, findSecret, secret_state, writeSecret } from '../../secrets.js'; | 3 | import { SECRET_KEYS, findSecret, secret_state, writeSecret } from '../../secrets.js'; |
| 4 | import { getPreviewString, saveTtsProviderSettings } from './index.js'; | 4 | import { getPreviewString, saveTtsProviderSettings } from './index.js'; |
| 5 | export { AzureTtsProvider }; | 5 | export { AzureTtsProvider }; |
| @@ -70,7 +70,19 @@ class AzureTtsProvider { | |||
| 70 | const popupText = 'Azure TTS API Key'; | 70 | const popupText = 'Azure TTS API Key'; |
| 71 | const savedKey = secret_state[SECRET_KEYS.AZURE_TTS] ? await findSecret(SECRET_KEYS.AZURE_TTS) : ''; | 71 | const savedKey = secret_state[SECRET_KEYS.AZURE_TTS] ? await findSecret(SECRET_KEYS.AZURE_TTS) : ''; |
| 72 | 72 | ||
| 73 | const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedKey); | 73 | const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedKey, { |
| 74 | customButtons: [{ | ||
| 75 | text: 'Remove Key', | ||
| 76 | appendAtEnd: true, | ||
| 77 | result: POPUP_RESULT.NEGATIVE, | ||
| 78 | action: async () => { | ||
| 79 | await writeSecret(SECRET_KEYS.AZURE_TTS, ''); | ||
| 80 | $('#azure_tts_key').toggleClass('success', secret_state[SECRET_KEYS.AZURE_TTS]); | ||
| 81 | toastr.success('API Key removed'); | ||
| 82 | await this.onRefreshClick(); | ||
| 83 | }, | ||
| 84 | }], | ||
| 85 | }); | ||
| 74 | 86 | ||
| 75 | if (key == false || key == '') { | 87 | if (key == false || key == '') { |
| 76 | return; | 88 | return; |
| @@ -9,6 +9,7 @@ import { SystemTtsProvider } from './system.js'; | |||
| 9 | import { NovelTtsProvider } from './novel.js'; | 9 | import { NovelTtsProvider } from './novel.js'; |
| 10 | import { power_user } from '../../power-user.js'; | 10 | import { power_user } from '../../power-user.js'; |
| 11 | import { OpenAITtsProvider } from './openai.js'; | 11 | import { OpenAITtsProvider } from './openai.js'; |
| 12 | import { OpenAICompatibleTtsProvider } from './openai-compatible.js'; | ||
| 12 | import { XTTSTtsProvider } from './xtts.js'; | 13 | import { XTTSTtsProvider } from './xtts.js'; |
| 13 | import { VITSTtsProvider } from './vits.js'; | 14 | import { VITSTtsProvider } from './vits.js'; |
| 14 | import { GSVITtsProvider } from './gsvi.js'; | 15 | import { GSVITtsProvider } from './gsvi.js'; |
| @@ -82,20 +83,21 @@ export function getPreviewString(lang) { | |||
| 82 | } | 83 | } |
| 83 | 84 | ||
| 84 | const ttsProviders = { | 85 | const ttsProviders = { |
| 85 | ElevenLabs: ElevenLabsTtsProvider, | 86 | AllTalk: AllTalkTtsProvider, |
| 86 | Silero: SileroTtsProvider, | 87 | Azure: AzureTtsProvider, |
| 87 | XTTSv2: XTTSTtsProvider, | ||
| 88 | VITS: VITSTtsProvider, | ||
| 89 | GSVI: GSVITtsProvider, | ||
| 90 | SBVits2: SBVits2TtsProvider, | ||
| 91 | System: SystemTtsProvider, | ||
| 92 | Coqui: CoquiTtsProvider, | 88 | Coqui: CoquiTtsProvider, |
| 93 | Edge: EdgeTtsProvider, | 89 | Edge: EdgeTtsProvider, |
| 90 | ElevenLabs: ElevenLabsTtsProvider, | ||
| 91 | GSVI: GSVITtsProvider, | ||
| 94 | Novel: NovelTtsProvider, | 92 | Novel: NovelTtsProvider, |
| 95 | OpenAI: OpenAITtsProvider, | 93 | OpenAI: OpenAITtsProvider, |
| 96 | AllTalk: AllTalkTtsProvider, | 94 | 'OpenAI Compatible': OpenAICompatibleTtsProvider, |
| 95 | SBVits2: SBVits2TtsProvider, | ||
| 96 | Silero: SileroTtsProvider, | ||
| 97 | SpeechT5: SpeechT5TtsProvider, | 97 | SpeechT5: SpeechT5TtsProvider, |
| 98 | Azure: AzureTtsProvider, | 98 | System: SystemTtsProvider, |
| 99 | VITS: VITSTtsProvider, | ||
| 100 | XTTSv2: XTTSTtsProvider, | ||
| 99 | }; | 101 | }; |
| 100 | let ttsProvider; | 102 | let ttsProvider; |
| 101 | let ttsProviderName; | 103 | let ttsProviderName; |
| @@ -0,0 +1,193 @@ | |||
| 1 | import { getRequestHeaders } from '../../../script.js'; | ||
| 2 | import { callGenericPopup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js'; | ||
| 3 | import { findSecret, SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js'; | ||
| 4 | import { getPreviewString, saveTtsProviderSettings } from './index.js'; | ||
| 5 | |||
| 6 | export { OpenAICompatibleTtsProvider }; | ||
| 7 | |||
| 8 | class OpenAICompatibleTtsProvider { | ||
| 9 | settings; | ||
| 10 | voices = []; | ||
| 11 | separator = ' . '; | ||
| 12 | |||
| 13 | audioElement = document.createElement('audio'); | ||
| 14 | |||
| 15 | defaultSettings = { | ||
| 16 | voiceMap: {}, | ||
| 17 | model: 'tts-1', | ||
| 18 | speed: 1, | ||
| 19 | available_voices: ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'], | ||
| 20 | provider_endpoint: 'http://127.0.0.1:8000/v1/audio/speech', | ||
| 21 | }; | ||
| 22 | |||
| 23 | get settingsHtml() { | ||
| 24 | let html = ` | ||
| 25 | <label for="openai_compatible_tts_endpoint">Provider Endpoint:</label> | ||
| 26 | <div class="flex-container alignItemsCenter"> | ||
| 27 | <div class="flex1"> | ||
| 28 | <input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/> | ||
| 29 | </div> | ||
| 30 | <div id="openai_compatible_tts_key" class="menu_button menu_button_icon"> | ||
| 31 | <i class="fa-solid fa-key"></i> | ||
| 32 | <span>API Key</span> | ||
| 33 | </div> | ||
| 34 | </div> | ||
| 35 | <label for="openai_compatible_model">Model:</label> | ||
| 36 | <input id="openai_compatible_model" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.model}"/> | ||
| 37 | <label for="openai_compatible_tts_voices">Available Voices (comma separated):</label> | ||
| 38 | <input id="openai_compatible_tts_voices" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.available_voices.join()}"/> | ||
| 39 | <label for="openai_compatible_tts_speed">Speed: <span id="openai_compatible_tts_speed_output"></span></label> | ||
| 40 | <input type="range" id="openai_compatible_tts_speed" value="1" min="0.25" max="4" step="0.05">`; | ||
| 41 | return html; | ||
| 42 | } | ||
| 43 | |||
| 44 | async loadSettings(settings) { | ||
| 45 | // Populate Provider UI given input settings | ||
| 46 | if (Object.keys(settings).length == 0) { | ||
| 47 | console.info('Using default TTS Provider settings'); | ||
| 48 | } | ||
| 49 | |||
| 50 | // Only accept keys defined in defaultSettings | ||
| 51 | this.settings = this.defaultSettings; | ||
| 52 | |||
| 53 | for (const key in settings) { | ||
| 54 | if (key in this.settings) { | ||
| 55 | this.settings[key] = settings[key]; | ||
| 56 | } else { | ||
| 57 | throw `Invalid setting passed to TTS Provider: ${key}`; | ||
| 58 | } | ||
| 59 | } | ||
| 60 | |||
| 61 | $('#openai_compatible_tts_endpoint').val(this.settings.provider_endpoint); | ||
| 62 | $('#openai_compatible_tts_endpoint').on('input', () => { this.onSettingsChange(); }); | ||
| 63 | |||
| 64 | $('#openai_compatible_model').val(this.defaultSettings.model); | ||
| 65 | $('#openai_compatible_model').on('input', () => { this.onSettingsChange(); }); | ||
| 66 | |||
| 67 | $('#openai_compatible_tts_voices').val(this.settings.available_voices.join()); | ||
| 68 | $('#openai_compatible_tts_voices').on('input', () => { this.onSettingsChange(); }); | ||
| 69 | |||
| 70 | $('#openai_compatible_tts_speed').val(this.settings.speed); | ||
| 71 | $('#openai_compatible_tts_speed').on('input', () => { | ||
| 72 | this.onSettingsChange(); | ||
| 73 | }); | ||
| 74 | |||
| 75 | $('#openai_compatible_tts_speed_output').text(this.settings.speed); | ||
| 76 | |||
| 77 | $('#openai_compatible_tts_key').toggleClass('success', secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS]); | ||
| 78 | $('#openai_compatible_tts_key').on('click', async () => { | ||
| 79 | const popupText = 'OpenAI-compatible TTS API Key'; | ||
| 80 | const savedKey = secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS] ? await findSecret(SECRET_KEYS.CUSTOM_OPENAI_TTS) : ''; | ||
| 81 | |||
| 82 | const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedKey, { | ||
| 83 | customButtons: [{ | ||
| 84 | text: 'Remove Key', | ||
| 85 | appendAtEnd: true, | ||
| 86 | result: POPUP_RESULT.NEGATIVE, | ||
| 87 | action: async () => { | ||
| 88 | await writeSecret(SECRET_KEYS.CUSTOM_OPENAI_TTS, ''); | ||
| 89 | $('#openai_compatible_tts_key').toggleClass('success', secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS]); | ||
| 90 | toastr.success('API Key removed'); | ||
| 91 | await this.onRefreshClick(); | ||
| 92 | }, | ||
| 93 | }], | ||
| 94 | }); | ||
| 95 | |||
| 96 | if (key == false || key == '') { | ||
| 97 | return; | ||
| 98 | } | ||
| 99 | |||
| 100 | await writeSecret(SECRET_KEYS.CUSTOM_OPENAI_TTS, String(key)); | ||
| 101 | |||
| 102 | toastr.success('API Key saved'); | ||
| 103 | $('#openai_compatible_tts_key').toggleClass('success', secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS]); | ||
| 104 | await this.onRefreshClick(); | ||
| 105 | }); | ||
| 106 | |||
| 107 | await this.checkReady(); | ||
| 108 | |||
| 109 | console.debug('OpenAI Compatible TTS: Settings loaded'); | ||
| 110 | } | ||
| 111 | |||
| 112 | onSettingsChange() { | ||
| 113 | // Update dynamically | ||
| 114 | this.settings.provider_endpoint = String($('#openai_compatible_tts_endpoint').val()); | ||
| 115 | this.settings.model = String($('#openai_compatible_model').val()); | ||
| 116 | this.settings.available_voices = String($('#openai_compatible_tts_voices').val()).split(','); | ||
| 117 | this.settings.speed = Number($('#openai_compatible_tts_speed').val()); | ||
| 118 | $('#openai_compatible_tts_speed_output').text(this.settings.speed); | ||
| 119 | saveTtsProviderSettings(); | ||
| 120 | } | ||
| 121 | |||
| 122 | async checkReady() { | ||
| 123 | await this.fetchTtsVoiceObjects(); | ||
| 124 | } | ||
| 125 | |||
| 126 | async onRefreshClick() { | ||
| 127 | return; | ||
| 128 | } | ||
| 129 | |||
| 130 | async getVoice(voiceName) { | ||
| 131 | if (this.voices.length == 0) { | ||
| 132 | this.voices = await this.fetchTtsVoiceObjects(); | ||
| 133 | } | ||
| 134 | const match = this.voices.filter( | ||
| 135 | oaicVoice => oaicVoice.name == voiceName, | ||
| 136 | )[0]; | ||
| 137 | if (!match) { | ||
| 138 | throw `TTS Voice name ${voiceName} not found`; | ||
| 139 | } | ||
| 140 | return match; | ||
| 141 | } | ||
| 142 | |||
| 143 | async generateTts(text, voiceId) { | ||
| 144 | const response = await this.fetchTtsGeneration(text, voiceId); | ||
| 145 | return response; | ||
| 146 | } | ||
| 147 | |||
| 148 | async fetchTtsVoiceObjects() { | ||
| 149 | return this.settings.available_voices.map(v => { | ||
| 150 | return { name: v, voice_id: v, lang: 'en-US' }; | ||
| 151 | }); | ||
| 152 | } | ||
| 153 | |||
| 154 | async previewTtsVoice(voiceId) { | ||
| 155 | this.audioElement.pause(); | ||
| 156 | this.audioElement.currentTime = 0; | ||
| 157 | |||
| 158 | const text = getPreviewString('en-US'); | ||
| 159 | const response = await this.fetchTtsGeneration(text, voiceId); | ||
| 160 | if (!response.ok) { | ||
| 161 | throw new Error(`HTTP ${response.status}`); | ||
| 162 | } | ||
| 163 | |||
| 164 | const audio = await response.blob(); | ||
| 165 | const url = URL.createObjectURL(audio); | ||
| 166 | this.audioElement.src = url; | ||
| 167 | this.audioElement.play(); | ||
| 168 | this.audioElement.onended = () => URL.revokeObjectURL(url); | ||
| 169 | } | ||
| 170 | |||
| 171 | async fetchTtsGeneration(inputText, voiceId) { | ||
| 172 | console.info(`Generating new TTS for voice_id ${voiceId}`); | ||
| 173 | const response = await fetch('/api/openai/custom/generate-voice', { | ||
| 174 | method: 'POST', | ||
| 175 | headers: getRequestHeaders(), | ||
| 176 | body: JSON.stringify({ | ||
| 177 | provider_endpoint: this.settings.provider_endpoint, | ||
| 178 | model: this.settings.model, | ||
| 179 | input: inputText, | ||
| 180 | voice: voiceId, | ||
| 181 | response_format: 'mp3', | ||
| 182 | speed: this.settings.speed, | ||
| 183 | }), | ||
| 184 | }); | ||
| 185 | |||
| 186 | if (!response.ok) { | ||
| 187 | toastr.error(response.statusText, 'TTS Generation Failed'); | ||
| 188 | throw new Error(`HTTP ${response.status}: ${await response.text()}`); | ||
| 189 | } | ||
| 190 | |||
| 191 | return response; | ||
| 192 | } | ||
| 193 | } | ||
| @@ -30,6 +30,7 @@ import { textgen_types, textgenerationwebui_settings } from '../../textgen-setti | |||
| 30 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; | 30 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
| 31 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; | 31 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| 32 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; | 32 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
| 33 | import { callGenericPopup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js'; | ||
| 33 | 34 | ||
| 34 | const MODULE_NAME = 'vectors'; | 35 | const MODULE_NAME = 'vectors'; |
| 35 | 36 | ||
| @@ -1299,11 +1300,30 @@ jQuery(async () => { | |||
| 1299 | saveSettingsDebounced(); | 1300 | saveSettingsDebounced(); |
| 1300 | toggleSettings(); | 1301 | toggleSettings(); |
| 1301 | }); | 1302 | }); |
| 1302 | $('#api_key_nomicai').on('change', () => { | 1303 | $('#api_key_nomicai').on('click', async () => { |
| 1303 | const nomicKey = String($('#api_key_nomicai').val()).trim(); | 1304 | const popupText = 'NomicAI API Key:'; |
| 1304 | if (nomicKey.length) { | 1305 | const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, '', { |
| 1305 | writeSecret(SECRET_KEYS.NOMICAI, nomicKey); | 1306 | customButtons: [{ |
| 1307 | text: 'Remove Key', | ||
| 1308 | appendAtEnd: true, | ||
| 1309 | result: POPUP_RESULT.NEGATIVE, | ||
| 1310 | action: async () => { | ||
| 1311 | await writeSecret(SECRET_KEYS.NOMICAI, ''); | ||
| 1312 | toastr.success('API Key removed'); | ||
| 1313 | $('#api_key_nomicai').toggleClass('success', !!secret_state[SECRET_KEYS.NOMICAI]); | ||
| 1314 | saveSettingsDebounced(); | ||
| 1315 | }, | ||
| 1316 | }], | ||
| 1317 | }); | ||
| 1318 | |||
| 1319 | if (!key) { | ||
| 1320 | return; | ||
| 1306 | } | 1321 | } |
| 1322 | |||
| 1323 | await writeSecret(SECRET_KEYS.NOMICAI, String(key)); | ||
| 1324 | $('#api_key_nomicai').toggleClass('success', !!secret_state[SECRET_KEYS.NOMICAI]); | ||
| 1325 | |||
| 1326 | toastr.success('API Key saved'); | ||
| 1307 | saveSettingsDebounced(); | 1327 | saveSettingsDebounced(); |
| 1308 | }); | 1328 | }); |
| 1309 | $('#vectors_togetherai_model').val(settings.togetherai_model).on('change', () => { | 1329 | $('#vectors_togetherai_model').val(settings.togetherai_model).on('change', () => { |
| @@ -1531,9 +1551,7 @@ jQuery(async () => { | |||
| 1531 | $('#dialogue_popup_input').val(presetModel); | 1551 | $('#dialogue_popup_input').val(presetModel); |
| 1532 | }); | 1552 | }); |
| 1533 | 1553 | ||
| 1534 | const validSecret = !!secret_state[SECRET_KEYS.NOMICAI]; | 1554 | $('#api_key_nomicai').toggleClass('success', !!secret_state[SECRET_KEYS.NOMICAI]); |
| 1535 | const placeholder = validSecret ? '✔️ Key saved' : '❌ Missing key'; | ||
| 1536 | $('#api_key_nomicai').attr('placeholder', placeholder); | ||
| 1537 | 1555 | ||
| 1538 | toggleSettings(); | 1556 | toggleSettings(); |
| 1539 | eventSource.on(event_types.MESSAGE_DELETED, onChatEvent); | 1557 | eventSource.on(event_types.MESSAGE_DELETED, onChatEvent); |
| @@ -103,17 +103,13 @@ | |||
| 103 | </span> | 103 | </span> |
| 104 | </small> | 104 | </small> |
| 105 | 105 | ||
| 106 | <div class="flex-container flexFlowColumn" id="nomicai_apiKey"> | 106 | <div class="flex-container alignItemsCenter" id="nomicai_apiKey"> |
| 107 | <label for="api_key_nomicai"> | 107 | <label for="api_key_nomicai" class="flex1"> |
| 108 | <span data-i18n="NomicAI API Key">NomicAI API Key</span> | 108 | <span data-i18n="NomicAI API Key">NomicAI API Key</span> |
| 109 | </label> | 109 | </label> |
| 110 | <div class="flex-container"> | 110 | <div id="api_key_nomicai" class="menu_button menu_button_icon"> |
| 111 | <input id="api_key_nomicai" name="api_key_nomicai" class="text_pole flex1 wide100p" maxlength="500" size="35" type="text" autocomplete="off"> | 111 | <i class="fa-solid fa-key"></i> |
| 112 | <div title="Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_nomicai"> | 112 | <span data-i18n="Click to set">Click to set</span> |
| 113 | </div> | ||
| 114 | </div> | ||
| 115 | <div data-for="api_key_nomicai" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you reload the page."> | ||
| 116 | For privacy reasons, your API key will be hidden after you reload the page. | ||
| 117 | </div> | 113 | </div> |
| 118 | </div> | 114 | </div> |
| 119 | 115 | ||
| @@ -33,6 +33,7 @@ export const SECRET_KEYS = { | |||
| 33 | HUGGINGFACE: 'api_key_huggingface', | 33 | HUGGINGFACE: 'api_key_huggingface', |
| 34 | STABILITY: 'api_key_stability', | 34 | STABILITY: 'api_key_stability', |
| 35 | BLOCKENTROPY: 'api_key_blockentropy', | 35 | BLOCKENTROPY: 'api_key_blockentropy', |
| 36 | CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts', | ||
| 36 | }; | 37 | }; |
| 37 | 38 | ||
| 38 | const INPUT_MAP = { | 39 | const INPUT_MAP = { |
| @@ -127,7 +128,7 @@ export async function writeSecret(key, value) { | |||
| 127 | const text = await response.text(); | 128 | const text = await response.text(); |
| 128 | 129 | ||
| 129 | if (text == 'ok') { | 130 | if (text == 'ok') { |
| 130 | secret_state[key] = true; | 131 | secret_state[key] = !!value; |
| 131 | updateSecretDisplay(); | 132 | updateSecretDisplay(); |
| 132 | } | 133 | } |
| 133 | } | 134 | } |
| @@ -282,4 +282,48 @@ router.post('/generate-image', jsonParser, async (request, response) => { | |||
| 282 | } | 282 | } |
| 283 | }); | 283 | }); |
| 284 | 284 | ||
| 285 | const custom = express.Router(); | ||
| 286 | |||
| 287 | custom.post('/generate-voice', jsonParser, async (request, response) => { | ||
| 288 | try { | ||
| 289 | const key = readSecret(request.user.directories, SECRET_KEYS.CUSTOM_OPENAI_TTS); | ||
| 290 | const { input, provider_endpoint, response_format, voice, speed, model } = request.body; | ||
| 291 | |||
| 292 | if (!provider_endpoint) { | ||
| 293 | console.log('No OpenAI-compatible TTS provider endpoint provided'); | ||
| 294 | return response.sendStatus(400); | ||
| 295 | } | ||
| 296 | |||
| 297 | const result = await fetch(provider_endpoint, { | ||
| 298 | method: 'POST', | ||
| 299 | headers: { | ||
| 300 | 'Content-Type': 'application/json', | ||
| 301 | Authorization: `Bearer ${key ?? ''}`, | ||
| 302 | }, | ||
| 303 | body: JSON.stringify({ | ||
| 304 | input: input ?? '', | ||
| 305 | response_format: response_format ?? 'mp3', | ||
| 306 | voice: voice ?? 'alloy', | ||
| 307 | speed: speed ?? 1, | ||
| 308 | model: model ?? 'tts-1', | ||
| 309 | }), | ||
| 310 | }); | ||
| 311 | |||
| 312 | if (!result.ok) { | ||
| 313 | const text = await result.text(); | ||
| 314 | console.log('OpenAI request failed', result.statusText, text); | ||
| 315 | return response.status(500).send(text); | ||
| 316 | } | ||
| 317 | |||
| 318 | const buffer = await result.arrayBuffer(); | ||
| 319 | response.setHeader('Content-Type', 'audio/mpeg'); | ||
| 320 | return response.send(Buffer.from(buffer)); | ||
| 321 | } catch (error) { | ||
| 322 | console.error('OpenAI TTS generation failed', error); | ||
| 323 | response.status(500).send('Internal server error'); | ||
| 324 | } | ||
| 325 | }); | ||
| 326 | |||
| 327 | router.use('/custom', custom); | ||
| 328 | |||
| 285 | module.exports = { router }; | 329 | module.exports = { router }; |
| @@ -45,6 +45,7 @@ const SECRET_KEYS = { | |||
| 45 | HUGGINGFACE: 'api_key_huggingface', | 45 | HUGGINGFACE: 'api_key_huggingface', |
| 46 | STABILITY: 'api_key_stability', | 46 | STABILITY: 'api_key_stability', |
| 47 | BLOCKENTROPY: 'api_key_blockentropy', | 47 | BLOCKENTROPY: 'api_key_blockentropy', |
| 48 | CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts', | ||
| 48 | }; | 49 | }; |
| 49 | 50 | ||
| 50 | // These are the keys that are safe to expose, even if allowKeysExposure is false | 51 | // These are the keys that are safe to expose, even if allowKeysExposure is false |