From 4e99c3e4cb2ec1837dc9dcb829ed514794bb93fa Mon Sep 17 00:00:00 2001 From: Gabraham Date: Fri, 3 May 2024 13:15:38 -0400 Subject: [PATCH 01/26] Disabled forced 4 spaces indented sublists for markdown formatting - For #2176 --- public/script.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/script.js b/public/script.js index 14079e155..a12786649 100644 --- a/public/script.js +++ b/public/script.js @@ -670,6 +670,7 @@ export function reloadMarkdownProcessor(render_formulas = false) { tables: true, parseImgDimensions: true, simpleLineBreaks: true, + disableForced4SpacesIndentedSublists: true, extensions: [ showdownKatex( { @@ -689,6 +690,7 @@ export function reloadMarkdownProcessor(render_formulas = false) { tables: true, underline: true, simpleLineBreaks: true, + disableForced4SpacesIndentedSublists: true, extensions: [markdownUnderscoreExt()], }); } From 8b7a858e1f7745e0db43dac4d4ad765249886a98 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 21 May 2024 20:45:28 +0300 Subject: [PATCH 02/26] Update readme.md --- .github/readme.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/readme.md b/.github/readme.md index ad3689c1a..77c941334 100644 --- a/.github/readme.md +++ b/.github/readme.md @@ -219,7 +219,11 @@ You will need two mandatory directory mappings and a port mapping to allow Silly #### Install command 1. Open your Command Line -2. Run the following command `docker create --name='sillytavern' --net='[DockerNet]' -e TZ="[TimeZone]" -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' 'ghcr.io/sillytavern/sillytavern:[version]' ` +2. Run the following command + +```bash +docker create --name='sillytavern' --net='[DockerNet]' -e TZ="[TimeZone]" -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' 'ghcr.io/sillytavern/sillytavern:[version]' +``` > Note that 8000 is a default listening port. Don't forget to use an appropriate port if you change it in the config. From 56392c17897f80e6411d63382bbdb659724e357f Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 21 May 2024 20:46:41 +0300 Subject: [PATCH 03/26] Update readme.md --- .github/readme.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/readme.md b/.github/readme.md index 77c941334..fe0b7f99f 100644 --- a/.github/readme.md +++ b/.github/readme.md @@ -221,9 +221,7 @@ You will need two mandatory directory mappings and a port mapping to allow Silly 1. Open your Command Line 2. Run the following command -```bash -docker create --name='sillytavern' --net='[DockerNet]' -e TZ="[TimeZone]" -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' 'ghcr.io/sillytavern/sillytavern:[version]' -``` +`docker create --name='sillytavern' --net='[DockerNet]' -e TZ="[TimeZone]" -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'` > Note that 8000 is a default listening port. Don't forget to use an appropriate port if you change it in the config. From 0371bf4e9fe0f1c6e82fb9aa688209a559748ba8 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 22 May 2024 01:36:38 +0300 Subject: [PATCH 04/26] Revoke 1-time object URLs --- public/script.js | 1 + public/scripts/extensions/quick-reply/src/ui/SettingsUi.js | 1 + public/scripts/extensions/tts/edge.js | 1 + public/scripts/extensions/tts/novel.js | 1 + public/scripts/extensions/tts/speecht5.js | 1 + public/scripts/utils.js | 1 + 6 files changed, 6 insertions(+) diff --git a/public/script.js b/public/script.js index 5f810845d..7dbe20253 100644 --- a/public/script.js +++ b/public/script.js @@ -9971,6 +9971,7 @@ jQuery(async function () { a.setAttribute('download', filename); document.body.appendChild(a); a.click(); + URL.revokeObjectURL(a.href); document.body.removeChild(a); } diff --git a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js index 9bd04b7a0..48a5d059d 100644 --- a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js +++ b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js @@ -357,6 +357,7 @@ export class SettingsUi { a.download = `${this.currentQrSet.name}.json`; a.click(); } + URL.revokeObjectURL(url); } selectQrSet(qrs) { diff --git a/public/scripts/extensions/tts/edge.js b/public/scripts/extensions/tts/edge.js index 4aeb935a8..cff090d0b 100644 --- a/public/scripts/extensions/tts/edge.js +++ b/public/scripts/extensions/tts/edge.js @@ -155,6 +155,7 @@ class EdgeTtsProvider { const url = URL.createObjectURL(audio); this.audioElement.src = url; this.audioElement.play(); + URL.revokeObjectURL(url); } /** diff --git a/public/scripts/extensions/tts/novel.js b/public/scripts/extensions/tts/novel.js index 0f43effe8..bab4f5e5a 100644 --- a/public/scripts/extensions/tts/novel.js +++ b/public/scripts/extensions/tts/novel.js @@ -180,6 +180,7 @@ class NovelTtsProvider { const url = URL.createObjectURL(audio); this.audioElement.src = url; this.audioElement.play(); + URL.revokeObjectURL(url); } async* fetchTtsGeneration(inputText, voiceId) { diff --git a/public/scripts/extensions/tts/speecht5.js b/public/scripts/extensions/tts/speecht5.js index 933074c9b..2ab19518a 100644 --- a/public/scripts/extensions/tts/speecht5.js +++ b/public/scripts/extensions/tts/speecht5.js @@ -60,6 +60,7 @@ class SpeechT5TtsProvider { const url = URL.createObjectURL(audio); this.audioElement.src = url; this.audioElement.play(); + URL.revokeObjectURL(url); } async loadSettings(settings) { diff --git a/public/scripts/utils.js b/public/scripts/utils.js index 63539513a..f3db706e6 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -139,6 +139,7 @@ export function download(content, fileName, contentType) { a.href = URL.createObjectURL(file); a.download = fileName; a.click(); + URL.revokeObjectURL(a.href); } /** From f5fccc0387a07d32a3916b85858facbf69c7b1bb Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 22 May 2024 01:37:51 +0300 Subject: [PATCH 05/26] Add Azure TTS service --- public/scripts/extensions/tts/azure.js | 207 +++++++++++++++++++++++++ public/scripts/extensions/tts/index.js | 2 + public/scripts/secrets.js | 1 + server.js | 3 + src/endpoints/azure.js | 92 +++++++++++ src/endpoints/secrets.js | 1 + 6 files changed, 306 insertions(+) create mode 100644 public/scripts/extensions/tts/azure.js create mode 100644 src/endpoints/azure.js diff --git a/public/scripts/extensions/tts/azure.js b/public/scripts/extensions/tts/azure.js new file mode 100644 index 000000000..abbf3ef33 --- /dev/null +++ b/public/scripts/extensions/tts/azure.js @@ -0,0 +1,207 @@ +import { callPopup, getRequestHeaders } from '../../../script.js'; +import { SECRET_KEYS, findSecret, secret_state, writeSecret } from '../../secrets.js'; +import { getPreviewString, saveTtsProviderSettings } from './index.js'; +export { AzureTtsProvider }; + +class AzureTtsProvider { + //########// + // Config // + //########// + + settings; + voices = []; + separator = ' . '; + audioElement = document.createElement('audio'); + + defaultSettings = { + region: '', + voiceMap: {}, + }; + + get settingsHtml() { + let html = ` +
+
+

+ Azure TTS Key +

+ +
+ + +
+
+ `; + return html; + } + + onSettingsChange() { + // Update dynamically + this.settings.region = String($('#azure_tts_region').val()); + // Reset voices + this.voices = []; + saveTtsProviderSettings(); + } + + async loadSettings(settings) { + // Populate Provider UI given input settings + if (Object.keys(settings).length == 0) { + console.info('Using default TTS Provider settings'); + } + + // Only accept keys defined in defaultSettings + this.settings = this.defaultSettings; + + for (const key in settings) { + if (key in this.settings) { + this.settings[key] = settings[key]; + } else { + throw `Invalid setting passed to TTS Provider: ${key}`; + } + } + + $('#azure_tts_region').val(this.settings.region).on('input', () => this.onSettingsChange()); + $('#azure_tts_key').toggleClass('success', secret_state[SECRET_KEYS.AZURE_TTS]); + $('#azure_tts_key').on('click', async () => { + const popupText = 'Azure TTS API Key'; + const savedKey = secret_state[SECRET_KEYS.AZURE_TTS] ? await findSecret(SECRET_KEYS.AZURE_TTS) : ''; + + const key = await callPopup(popupText, 'input', savedKey); + + if (key == false || key == '') { + return; + } + + await writeSecret(SECRET_KEYS.AZURE_TTS, key); + + toastr.success('API Key saved'); + $('#azure_tts_key').addClass('success'); + await this.onRefreshClick(); + }); + + try { + await this.checkReady(); + console.debug('Azure: Settings loaded'); + } catch { + console.debug('Azure: Settings loaded, but not ready'); + } + } + + // Perform a simple readiness check by trying to fetch voiceIds + async checkReady() { + if (secret_state[SECRET_KEYS.AZURE_TTS]) { + await this.fetchTtsVoiceObjects(); + } else { + this.voices = []; + } + } + + async onRefreshClick() { + await this.checkReady(); + } + + //#################// + // TTS Interfaces // + //#################// + + async getVoice(voiceName) { + if (this.voices.length == 0) { + this.voices = await this.fetchTtsVoiceObjects(); + } + const match = this.voices.filter( + voice => voice.name == voiceName, + )[0]; + if (!match) { + throw `TTS Voice name ${voiceName} not found`; + } + return match; + } + + async generateTts(text, voiceId) { + const response = await this.fetchTtsGeneration(text, voiceId); + return response; + } + + //###########// + // API CALLS // + //###########// + async fetchTtsVoiceObjects() { + if (!secret_state[SECRET_KEYS.AZURE_TTS]) { + console.warn('Azure TTS API Key not set'); + return []; + } + + if (!this.settings.region) { + console.warn('Azure TTS region not set'); + return []; + } + + const response = await fetch('/api/azure/list', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ + region: this.settings.region, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${await response.text()}`); + } + let responseJson = await response.json(); + responseJson = responseJson + .sort((a, b) => a.Locale.localeCompare(b.Locale) || a.ShortName.localeCompare(b.ShortName)) + .map(x => ({ name: x.ShortName, voice_id: x.ShortName, preview_url: false, lang: x.Locale })); + return responseJson; + } + + /** + * Preview TTS for a given voice ID. + * @param {string} id Voice ID + */ + async previewTtsVoice(id) { + this.audioElement.pause(); + this.audioElement.currentTime = 0; + const voice = await this.getVoice(id); + const text = getPreviewString(voice.lang); + const response = await this.fetchTtsGeneration(text, id); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${await response.text()}`); + } + + const audio = await response.blob(); + const url = URL.createObjectURL(audio); + this.audioElement.src = url; + this.audioElement.play(); + URL.revokeObjectURL(url); + } + + async fetchTtsGeneration(text, voiceId) { + if (!secret_state[SECRET_KEYS.AZURE_TTS]) { + throw new Error('Azure TTS API Key not set'); + } + + if (!this.settings.region) { + throw new Error('Azure TTS region not set'); + } + + const response = await fetch('/api/azure/generate', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ + text: text, + voice: voiceId, + region: this.settings.region, + }), + }); + + if (!response.ok) { + toastr.error(response.statusText, 'TTS Generation Failed'); + throw new Error(`HTTP ${response.status}: ${await response.text()}`); + } + + return response; + } +} diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index f73e9d48f..1ac1edd8b 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -13,6 +13,7 @@ import { XTTSTtsProvider } from './xtts.js'; import { GSVITtsProvider } from './gsvi.js'; import { AllTalkTtsProvider } from './alltalk.js'; import { SpeechT5TtsProvider } from './speecht5.js'; +import { AzureTtsProvider } from './azure.js'; import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; @@ -83,6 +84,7 @@ const ttsProviders = { OpenAI: OpenAITtsProvider, AllTalk: AllTalkTtsProvider, SpeechT5: SpeechT5TtsProvider, + Azure: AzureTtsProvider, }; let ttsProvider; let ttsProviderName; diff --git a/public/scripts/secrets.js b/public/scripts/secrets.js index 4d7a6ebf1..cb4477a78 100644 --- a/public/scripts/secrets.js +++ b/public/scripts/secrets.js @@ -27,6 +27,7 @@ export const SECRET_KEYS = { COHERE: 'api_key_cohere', PERPLEXITY: 'api_key_perplexity', GROQ: 'api_key_groq', + AZURE_TTS: 'api_key_azure_tts', }; const INPUT_MAP = { diff --git a/server.js b/server.js index e658d7b5e..6f67dc287 100644 --- a/server.js +++ b/server.js @@ -519,6 +519,9 @@ app.use('/api/backends/scale-alt', require('./src/endpoints/backends/scale-alt') // Speech (text-to-speech and speech-to-text) app.use('/api/speech', require('./src/endpoints/speech').router); +// Azure TTS +app.use('/api/azure', require('./src/endpoints/azure').router); + const tavernUrl = new URL( (cliArguments.ssl ? 'https://' : 'http://') + (listen ? '0.0.0.0' : '127.0.0.1') + diff --git a/src/endpoints/azure.js b/src/endpoints/azure.js new file mode 100644 index 000000000..4c3b34d5b --- /dev/null +++ b/src/endpoints/azure.js @@ -0,0 +1,92 @@ +const { readSecret, SECRET_KEYS } = require('./secrets'); +const fetch = require('node-fetch').default; +const express = require('express'); +const { jsonParser } = require('../express-common'); + +const router = express.Router(); + +router.post('/list', jsonParser, async (req, res) => { + try { + const key = readSecret(req.user.directories, SECRET_KEYS.AZURE_TTS); + + if (!key) { + console.error('Azure TTS API Key not set'); + return res.sendStatus(403); + } + + const region = req.body.region; + + if (!region) { + console.error('Azure TTS region not set'); + return res.sendStatus(400); + } + + const url = `https://${region}.tts.speech.microsoft.com/cognitiveservices/voices/list`; + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Ocp-Apim-Subscription-Key': key, + }, + }); + + if (!response.ok) { + console.error('Azure Request failed', response.status, response.statusText); + return res.sendStatus(500); + } + + const voices = await response.json(); + return res.json(voices); + } catch (error) { + console.error('Azure Request failed', error); + return res.sendStatus(500); + } +}); + +router.post('/generate', jsonParser, async (req, res) => { + try { + const key = readSecret(req.user.directories, SECRET_KEYS.AZURE_TTS); + + if (!key) { + console.error('Azure TTS API Key not set'); + return res.sendStatus(403); + } + + const { text, voice, region } = req.body; + if (!text || !voice || !region) { + console.error('Missing required parameters'); + return res.sendStatus(400); + } + + const url = `https://${region}.tts.speech.microsoft.com/cognitiveservices/v1`; + const lang = String(voice).split('-').slice(0, 2).join('-'); + const escapedText = String(text).replace(/&/g, '&').replace(//g, '>'); + const ssml = `${escapedText}`; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Ocp-Apim-Subscription-Key': key, + 'Content-Type': 'application/ssml+xml', + 'X-Microsoft-OutputFormat': 'ogg-48khz-16bit-mono-opus', + }, + body: ssml, + }); + + if (!response.ok) { + console.error('Azure Request failed', response.status, response.statusText); + return res.sendStatus(500); + } + + const audio = await response.buffer(); + res.set('Content-Type', 'audio/ogg'); + return res.send(audio); + } catch (error) { + console.error('Azure Request failed', error); + return res.sendStatus(500); + } +}); + +module.exports = { + router, +}; diff --git a/src/endpoints/secrets.js b/src/endpoints/secrets.js index 1a1ac3746..9bf2eb765 100644 --- a/src/endpoints/secrets.js +++ b/src/endpoints/secrets.js @@ -39,6 +39,7 @@ const SECRET_KEYS = { COHERE: 'api_key_cohere', PERPLEXITY: 'api_key_perplexity', GROQ: 'api_key_groq', + AZURE_TTS: 'api_key_azure_tts', }; // These are the keys that are safe to expose, even if allowKeysExposure is false From a12df762a091678ca811986d9c329a0df285f93b Mon Sep 17 00:00:00 2001 From: kingbri Date: Tue, 21 May 2024 23:35:29 -0400 Subject: [PATCH 06/26] Textgen: Add speculative_ngram for TabbyAPI Speculative ngram allows for a different method of speculative decoding. Using a draft model is still preferred. Signed-off-by: kingbri --- public/index.html | 7 +++++++ public/scripts/textgen-settings.js | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/public/index.html b/public/index.html index d67c78a71..016147616 100644 --- a/public/index.html +++ b/public/index.html @@ -1405,6 +1405,13 @@
+