mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'dev' of https://github.com/Cohee1207/SillyTavern into dev
This commit is contained in:
@@ -1382,7 +1382,7 @@
|
|||||||
</h4>
|
</h4>
|
||||||
<small class="horde_multiple_hint">You can select multiple models.<br>Avoid sending
|
<small class="horde_multiple_hint">You can select multiple models.<br>Avoid sending
|
||||||
sensitive information to the Horde. <a id="horde_privacy_disclaimer" target="_blank"
|
sensitive information to the Horde. <a id="horde_privacy_disclaimer" target="_blank"
|
||||||
href="/notes#horde">Learn more</a></small>
|
href="https://docs.sillytavern.app/usage/guidebook/#horde">Learn more</a></small>
|
||||||
<select id="horde_model" multiple>
|
<select id="horde_model" multiple>
|
||||||
<option>-- Horde models not loaded --</option>
|
<option>-- Horde models not loaded --</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -1416,7 +1416,7 @@
|
|||||||
<span>
|
<span>
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>
|
||||||
<span data-i18n="Follow">Follow</span> <a href="/notes#apikey"
|
<span data-i18n="Follow">Follow</span> <a href="https://docs.sillytavern.app/usage/guidebook/#api-key"
|
||||||
class="notes-link" target="_blank"> <span data-i18n="these directions">these
|
class="notes-link" target="_blank"> <span data-i18n="these directions">these
|
||||||
directions</span> </a> <span data-i18n="to get your NovelAI API key.">to
|
directions</span> </a> <span data-i18n="to get your NovelAI API key.">to
|
||||||
get your NovelAI API key.</span>
|
get your NovelAI API key.</span>
|
||||||
@@ -1437,7 +1437,7 @@
|
|||||||
<input id="api_button_novel" class="menu_button" type="submit" value="Connect">
|
<input id="api_button_novel" class="menu_button" type="submit" value="Connect">
|
||||||
<div id="api_loading_novel" class="api-load-icon fa-solid fa-hourglass fa-spin"></div>
|
<div id="api_loading_novel" class="api-load-icon fa-solid fa-hourglass fa-spin"></div>
|
||||||
<h4><span data-i18n="Novel AI Model">Novel AI Model</span>
|
<h4><span data-i18n="Novel AI Model">Novel AI Model</span>
|
||||||
<a href="/notes#models" class="notes-link" target="_blank">
|
<a href="https://docs.sillytavern.app/usage/guidebook/#models" class="notes-link" target="_blank">
|
||||||
<span class="note-link-span">?</span>
|
<span class="note-link-span">?</span>
|
||||||
</a>
|
</a>
|
||||||
</h4>
|
</h4>
|
||||||
@@ -1490,7 +1490,7 @@
|
|||||||
<label for="use_window_ai" class="checkbox_label">
|
<label for="use_window_ai" class="checkbox_label">
|
||||||
<input id="use_window_ai" type="checkbox" />
|
<input id="use_window_ai" type="checkbox" />
|
||||||
Use Window.ai
|
Use Window.ai
|
||||||
<a href="/notes#windowai" class="notes-link" target="_blank">
|
<a href="https://docs.sillytavern.app/usage/guidebook/#windowai" class="notes-link" target="_blank">
|
||||||
<span class="note-link-span">?</span>
|
<span class="note-link-span">?</span>
|
||||||
</a>
|
</a>
|
||||||
</label>
|
</label>
|
||||||
@@ -1545,8 +1545,7 @@
|
|||||||
<span>
|
<span>
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>
|
||||||
Follow<a href="/notes#apikey-2" class="notes-link" target="_blank"> these directions
|
Follow <a href="https://docs.sillytavern.app/usage/guidebook/#api-key-2" class="notes-link" target="_blank">these directions</a> to get your 'p-b cookie'
|
||||||
</a> to get your 'p-b cookie'
|
|
||||||
</li>
|
</li>
|
||||||
<li>Enter it in the box below:</li>
|
<li>Enter it in the box below:</li>
|
||||||
</ol>
|
</ol>
|
||||||
@@ -1736,6 +1735,8 @@
|
|||||||
<option value="1">GPT-3 (OpenAI)</option>
|
<option value="1">GPT-3 (OpenAI)</option>
|
||||||
<option value="2">GPT-3 (Alternative / Classic)</option>
|
<option value="2">GPT-3 (Alternative / Classic)</option>
|
||||||
<option value="3">Sentencepiece (LLaMA)</option>
|
<option value="3">Sentencepiece (LLaMA)</option>
|
||||||
|
<option value="4">NerdStash (NovelAI Krake)</option>
|
||||||
|
<option value="5">NerdStash v2 (NovelAI Clio)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="range-block">
|
<div class="range-block">
|
||||||
|
109
public/script.js
109
public/script.js
@@ -119,7 +119,6 @@ import {
|
|||||||
end_trim_to_sentence,
|
end_trim_to_sentence,
|
||||||
countOccurrences,
|
countOccurrences,
|
||||||
isOdd,
|
isOdd,
|
||||||
isElementInViewport,
|
|
||||||
sortMoments,
|
sortMoments,
|
||||||
timestampToMoment,
|
timestampToMoment,
|
||||||
download,
|
download,
|
||||||
@@ -481,22 +480,33 @@ function getTokenCount(str, padding = undefined) {
|
|||||||
case tokenizers.CLASSIC:
|
case tokenizers.CLASSIC:
|
||||||
return encode(str).length + padding;
|
return encode(str).length + padding;
|
||||||
case tokenizers.LLAMA:
|
case tokenizers.LLAMA:
|
||||||
let tokenCount = 0;
|
return countTokensRemote('/tokenize_llama', str, padding);
|
||||||
jQuery.ajax({
|
case tokenizers.NERD:
|
||||||
async: false,
|
return countTokensRemote('/tokenize_nerdstash', str, padding);
|
||||||
type: 'POST', //
|
case tokenizers.NERD2:
|
||||||
url: `/tokenize_llama`,
|
return countTokensRemote('/tokenize_nerdstash_v2', str, padding);
|
||||||
data: JSON.stringify({ text: str }),
|
default:
|
||||||
dataType: "json",
|
console.warn("Unknown tokenizer type", tokenizerType);
|
||||||
contentType: "application/json",
|
return Math.ceil(str.length / CHARACTERS_PER_TOKEN_RATIO) + padding;
|
||||||
success: function (data) {
|
|
||||||
tokenCount = data.count;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return tokenCount + padding;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function countTokensRemote(endpoint, str, padding) {
|
||||||
|
let tokenCount = 0;
|
||||||
|
jQuery.ajax({
|
||||||
|
async: false,
|
||||||
|
type: 'POST',
|
||||||
|
url: endpoint,
|
||||||
|
data: JSON.stringify({ text: str }),
|
||||||
|
dataType: "json",
|
||||||
|
contentType: "application/json",
|
||||||
|
success: function (data) {
|
||||||
|
tokenCount = data.count;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return tokenCount + padding;
|
||||||
|
}
|
||||||
|
|
||||||
function reloadMarkdownProcessor(render_formulas = false) {
|
function reloadMarkdownProcessor(render_formulas = false) {
|
||||||
if (render_formulas) {
|
if (render_formulas) {
|
||||||
converter = new showdown.Converter({
|
converter = new showdown.Converter({
|
||||||
@@ -2589,12 +2599,14 @@ function getMaxContextSize() {
|
|||||||
} else {
|
} else {
|
||||||
this_max_context = Number(max_context);
|
this_max_context = Number(max_context);
|
||||||
if (nai_settings.model_novel == 'krake-v2') {
|
if (nai_settings.model_novel == 'krake-v2') {
|
||||||
this_max_context -= 160;
|
// Krake has a max context of 2048
|
||||||
|
// Should be used with nerdstash tokenizer for best results
|
||||||
|
this_max_context = Math.min(max_context, 2048);
|
||||||
}
|
}
|
||||||
if (nai_settings.model_novel == 'clio-v1') {
|
if (nai_settings.model_novel == 'clio-v1') {
|
||||||
// Clio has a max context of 8192
|
// Clio has a max context of 8192
|
||||||
// TODO: Evaluate the relevance of nerdstash-v1 tokenizer, changes quite a bit.
|
// Should be used with nerdstash_v2 tokenizer for best results
|
||||||
this_max_context = 8192 - 60 - 160;
|
this_max_context = Math.min(max_context, 8192);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3397,7 +3409,15 @@ async function renameCharacter() {
|
|||||||
|
|
||||||
// Also rename as a group member
|
// Also rename as a group member
|
||||||
await renameGroupMember(oldAvatar, newAvatar, newValue);
|
await renameGroupMember(oldAvatar, newAvatar, newValue);
|
||||||
callPopup('<h3>Character renamed!</h3>Sprites folder (if any) should be renamed manually.', 'text');
|
const renamePastChatsConfirm = await callPopup(`<h3>Character renamed!</h3>
|
||||||
|
<p>Past chats will still contain the old character name. Would you like to update the character name in previous chats as well?</p>
|
||||||
|
<i><b>Sprites folder (if any) should be renamed manually.</b></i>`, 'confirm');
|
||||||
|
|
||||||
|
if (renamePastChatsConfirm) {
|
||||||
|
await renamePastChats(newAvatar, newValue);
|
||||||
|
await reloadCurrentChat();
|
||||||
|
toastr.success('Character renamed and past chats updated!');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new Error('Newly renamed character was lost?');
|
throw new Error('Newly renamed character was lost?');
|
||||||
@@ -3415,6 +3435,59 @@ async function renameCharacter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function renamePastChats(newAvatar, newValue) {
|
||||||
|
const pastChats = await getPastCharacterChats();
|
||||||
|
|
||||||
|
for (const { file_name } of pastChats) {
|
||||||
|
try {
|
||||||
|
const fileNameWithoutExtension = file_name.replace('.jsonl', '');
|
||||||
|
const getChatResponse = await fetch('/getchat', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getRequestHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
|
ch_name: newValue,
|
||||||
|
file_name: fileNameWithoutExtension,
|
||||||
|
avatar_url: newAvatar,
|
||||||
|
}),
|
||||||
|
cache: 'no-cache',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (getChatResponse.ok) {
|
||||||
|
const currentChat = await getChatResponse.json();
|
||||||
|
|
||||||
|
for (const message of currentChat) {
|
||||||
|
if (message.is_user || message.is_system || message.extra?.type == system_message_types.NARRATOR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.name !== undefined) {
|
||||||
|
message.name = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveChatResponse = await fetch('/savechat', {
|
||||||
|
method: "POST",
|
||||||
|
headers: getRequestHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
|
ch_name: newValue,
|
||||||
|
file_name: fileNameWithoutExtension,
|
||||||
|
chat: currentChat,
|
||||||
|
avatar_url: newAvatar,
|
||||||
|
}),
|
||||||
|
cache: 'no-cache',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!saveChatResponse.ok) {
|
||||||
|
throw new Error('Could not save chat');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toastr.error(`Past chat could not be updated: ${file_name}`);
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveChat(chat_name, withMetadata) {
|
async function saveChat(chat_name, withMetadata) {
|
||||||
const metadata = { ...chat_metadata, ...(withMetadata || {}) };
|
const metadata = { ...chat_metadata, ...(withMetadata || {}) };
|
||||||
let file_name = chat_name ?? characters[this_chid].chat;
|
let file_name = chat_name ?? characters[this_chid].chat;
|
||||||
|
110
public/scripts/extensions/speech-recognition/index.js
Normal file
110
public/scripts/extensions/speech-recognition/index.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
// Borrowed from Agnai (AGPLv3)
|
||||||
|
// https://github.com/agnaistic/agnai/blob/dev/web/pages/Chat/components/SpeechRecognitionRecorder.tsx
|
||||||
|
function capitalizeInterim(interimTranscript) {
|
||||||
|
let capitalizeIndex = -1;
|
||||||
|
if (interimTranscript.length > 2 && interimTranscript[0] === ' ') capitalizeIndex = 1;
|
||||||
|
else if (interimTranscript.length > 1) capitalizeIndex = 0;
|
||||||
|
if (capitalizeIndex > -1) {
|
||||||
|
const spacing = capitalizeIndex > 0 ? ' '.repeat(capitalizeIndex - 1) : '';
|
||||||
|
const capitalized = interimTranscript[capitalizeIndex].toLocaleUpperCase();
|
||||||
|
const rest = interimTranscript.substring(capitalizeIndex + 1);
|
||||||
|
interimTranscript = spacing + capitalized + rest;
|
||||||
|
}
|
||||||
|
return interimTranscript;
|
||||||
|
}
|
||||||
|
|
||||||
|
function composeValues(previous, interim) {
|
||||||
|
let spacing = '';
|
||||||
|
if (previous.endsWith('.')) spacing = ' ';
|
||||||
|
return previous + spacing + interim;
|
||||||
|
}
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
$.fn.speechRecognitionPlugin = function (options) {
|
||||||
|
const settings = $.extend({
|
||||||
|
grammar: '' // Custom grammar
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
const speechRecognition = window.SpeechRecognition || webkitSpeechRecognition;
|
||||||
|
const speechRecognitionList = window.SpeechGrammarList || webkitSpeechGrammarList;
|
||||||
|
|
||||||
|
if (!speechRecognition) {
|
||||||
|
console.warn('Speech recognition is not supported in this browser.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recognition = new speechRecognition();
|
||||||
|
|
||||||
|
if (settings.grammar) {
|
||||||
|
speechRecognitionList.addFromString(settings.grammar, 1);
|
||||||
|
recognition.grammars = speechRecognitionList;
|
||||||
|
}
|
||||||
|
|
||||||
|
recognition.continuous = true;
|
||||||
|
recognition.interimResults = true;
|
||||||
|
// TODO: This should be configurable.
|
||||||
|
recognition.lang = 'en-US'; // Set the language to English (US).
|
||||||
|
|
||||||
|
const $textarea = this;
|
||||||
|
const $button = $('<div class="fa-solid fa-microphone speech-toggle" title="Click to speak"></div>');
|
||||||
|
$('#send_but_sheld').prepend($button);
|
||||||
|
|
||||||
|
let listening = false;
|
||||||
|
$button.on('click', function () {
|
||||||
|
if (listening) {
|
||||||
|
recognition.stop();
|
||||||
|
} else {
|
||||||
|
recognition.start();
|
||||||
|
}
|
||||||
|
listening = !listening;
|
||||||
|
});
|
||||||
|
|
||||||
|
let initialText = '';
|
||||||
|
|
||||||
|
recognition.onresult = function (speechEvent) {
|
||||||
|
let finalTranscript = '';
|
||||||
|
let interimTranscript = ''
|
||||||
|
|
||||||
|
for (let i = speechEvent.resultIndex; i < speechEvent.results.length; ++i) {
|
||||||
|
const transcript = speechEvent.results[i][0].transcript;
|
||||||
|
|
||||||
|
if (speechEvent.results[i].isFinal) {
|
||||||
|
let interim = capitalizeInterim(transcript);
|
||||||
|
if (interim != '') {
|
||||||
|
let final = finalTranscript;
|
||||||
|
final = composeValues(final, interim) + '.';
|
||||||
|
finalTranscript = final;
|
||||||
|
recognition.abort();
|
||||||
|
listening = false;
|
||||||
|
}
|
||||||
|
interimTranscript = ' ';
|
||||||
|
} else {
|
||||||
|
interimTranscript += transcript;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interimTranscript = capitalizeInterim(interimTranscript);
|
||||||
|
|
||||||
|
$textarea.val(initialText + finalTranscript + interimTranscript);
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onerror = function (event) {
|
||||||
|
console.error('Error occurred in recognition:', event.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onend = function () {
|
||||||
|
listening = false;
|
||||||
|
$button.toggleClass('fa-microphone fa-microphone-slash');
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onstart = function () {
|
||||||
|
initialText = $textarea.val();
|
||||||
|
$button.toggleClass('fa-microphone fa-microphone-slash');
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}(jQuery));
|
||||||
|
|
||||||
|
jQuery(() => {
|
||||||
|
const $textarea = $('#send_textarea');
|
||||||
|
$textarea.speechRecognitionPlugin();
|
||||||
|
});
|
11
public/scripts/extensions/speech-recognition/manifest.json
Normal file
11
public/scripts/extensions/speech-recognition/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"display_name": "Speech Recognition",
|
||||||
|
"loading_order": 13,
|
||||||
|
"requires": [],
|
||||||
|
"optional": [],
|
||||||
|
"js": "index.js",
|
||||||
|
"css": "style.css",
|
||||||
|
"author": "Cohee#1207",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||||
|
}
|
3
public/scripts/extensions/speech-recognition/style.css
Normal file
3
public/scripts/extensions/speech-recognition/style.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.speech-toggle {
|
||||||
|
display: flex;
|
||||||
|
}
|
@@ -84,7 +84,7 @@ class EdgeTtsProvider {
|
|||||||
url.pathname = `/api/edge-tts/list`
|
url.pathname = `/api/edge-tts/list`
|
||||||
const response = await doExtrasFetch(url)
|
const response = await doExtrasFetch(url)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
|
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
|
||||||
}
|
}
|
||||||
let responseJson = await response.json()
|
let responseJson = await response.json()
|
||||||
responseJson = responseJson
|
responseJson = responseJson
|
||||||
@@ -101,7 +101,7 @@ class EdgeTtsProvider {
|
|||||||
const text = getPreviewString(voice.lang);
|
const text = getPreviewString(voice.lang);
|
||||||
const response = await this.fetchTtsGeneration(text, id)
|
const response = await this.fetchTtsGeneration(text, id)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
|
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const audio = await response.blob();
|
const audio = await response.blob();
|
||||||
@@ -127,14 +127,14 @@ class EdgeTtsProvider {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
|
toastr.error(response.statusText, 'TTS Generation Failed');
|
||||||
|
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function throwIfModuleMissing() {
|
function throwIfModuleMissing() {
|
||||||
if (!modules.includes('edge-tts')) {
|
if (!modules.includes('edge-tts')) {
|
||||||
|
|
||||||
toastr.error(`Edge TTS module not loaded. Add edge-tts to enable-modules and restart the Extras API.`)
|
toastr.error(`Edge TTS module not loaded. Add edge-tts to enable-modules and restart the Extras API.`)
|
||||||
throw new Error(`Edge TTS module not loaded.`)
|
throw new Error(`Edge TTS module not loaded.`)
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@ class ElevenLabsTtsProvider {
|
|||||||
settings
|
settings
|
||||||
voices = []
|
voices = []
|
||||||
separator = ' ... ... ... '
|
separator = ' ... ... ... '
|
||||||
|
|
||||||
get settings() {
|
get settings() {
|
||||||
return this.settings
|
return this.settings
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ class ElevenLabsTtsProvider {
|
|||||||
this.settings.similarity_boost = $('#elevenlabs_tts_similarity_boost').val()
|
this.settings.similarity_boost = $('#elevenlabs_tts_similarity_boost').val()
|
||||||
this.settings.multilingual = $('#elevenlabs_tts_multilingual').prop('checked')
|
this.settings.multilingual = $('#elevenlabs_tts_multilingual').prop('checked')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
loadSettings(settings) {
|
loadSettings(settings) {
|
||||||
// Pupulate Provider UI given input settings
|
// Pupulate Provider UI given input settings
|
||||||
@@ -75,8 +75,8 @@ class ElevenLabsTtsProvider {
|
|||||||
throw error
|
throw error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async updateApiKey() {
|
async updateApiKey() {
|
||||||
// Using this call to validate API key
|
// Using this call to validate API key
|
||||||
this.settings.apiKey = $('#elevenlabs_tts_api_key').val()
|
this.settings.apiKey = $('#elevenlabs_tts_api_key').val()
|
||||||
@@ -108,7 +108,7 @@ class ElevenLabsTtsProvider {
|
|||||||
|
|
||||||
async generateTts(text, voiceId){
|
async generateTts(text, voiceId){
|
||||||
const historyId = await this.findTtsGenerationInHistory(text, voiceId)
|
const historyId = await this.findTtsGenerationInHistory(text, voiceId)
|
||||||
|
|
||||||
let response
|
let response
|
||||||
if (historyId) {
|
if (historyId) {
|
||||||
console.debug(`Found existing TTS generation with id ${historyId}`)
|
console.debug(`Found existing TTS generation with id ${historyId}`)
|
||||||
@@ -119,11 +119,11 @@ class ElevenLabsTtsProvider {
|
|||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
//###################//
|
//###################//
|
||||||
// Helper Functions //
|
// Helper Functions //
|
||||||
//###################//
|
//###################//
|
||||||
|
|
||||||
async findTtsGenerationInHistory(message, voiceId) {
|
async findTtsGenerationInHistory(message, voiceId) {
|
||||||
const ttsHistory = await this.fetchTtsHistory()
|
const ttsHistory = await this.fetchTtsHistory()
|
||||||
for (const history of ttsHistory) {
|
for (const history of ttsHistory) {
|
||||||
@@ -149,7 +149,7 @@ class ElevenLabsTtsProvider {
|
|||||||
headers: headers
|
headers: headers
|
||||||
})
|
})
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
|
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
|
||||||
}
|
}
|
||||||
const responseJson = await response.json()
|
const responseJson = await response.json()
|
||||||
return responseJson.voices
|
return responseJson.voices
|
||||||
@@ -166,7 +166,7 @@ class ElevenLabsTtsProvider {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
|
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
|
||||||
}
|
}
|
||||||
return response.json()
|
return response.json()
|
||||||
}
|
}
|
||||||
@@ -193,7 +193,8 @@ class ElevenLabsTtsProvider {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
|
toastr.error(response.statusText, 'TTS Generation Failed');
|
||||||
|
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
@@ -209,7 +210,7 @@ class ElevenLabsTtsProvider {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
|
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
@@ -222,7 +223,7 @@ class ElevenLabsTtsProvider {
|
|||||||
headers: headers
|
headers: headers
|
||||||
})
|
})
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
|
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
|
||||||
}
|
}
|
||||||
const responseJson = await response.json()
|
const responseJson = await response.json()
|
||||||
return responseJson.history
|
return responseJson.history
|
||||||
|
@@ -5,6 +5,7 @@ import { EdgeTtsProvider } from './edge.js'
|
|||||||
import { ElevenLabsTtsProvider } from './elevenlabs.js'
|
import { ElevenLabsTtsProvider } from './elevenlabs.js'
|
||||||
import { SileroTtsProvider } from './silerotts.js'
|
import { SileroTtsProvider } from './silerotts.js'
|
||||||
import { SystemTtsProvider } from './system.js'
|
import { SystemTtsProvider } from './system.js'
|
||||||
|
import { NovelTtsProvider } from './novel.js'
|
||||||
|
|
||||||
const UPDATE_INTERVAL = 1000
|
const UPDATE_INTERVAL = 1000
|
||||||
|
|
||||||
@@ -62,6 +63,7 @@ let ttsProviders = {
|
|||||||
Silero: SileroTtsProvider,
|
Silero: SileroTtsProvider,
|
||||||
System: SystemTtsProvider,
|
System: SystemTtsProvider,
|
||||||
Edge: EdgeTtsProvider,
|
Edge: EdgeTtsProvider,
|
||||||
|
Novel: NovelTtsProvider,
|
||||||
}
|
}
|
||||||
let ttsProvider
|
let ttsProvider
|
||||||
let ttsProviderName
|
let ttsProviderName
|
||||||
@@ -244,7 +246,7 @@ async function playAudioData(audioBlob) {
|
|||||||
window['tts_preview'] = function (id) {
|
window['tts_preview'] = function (id) {
|
||||||
const audio = document.getElementById(id)
|
const audio = document.getElementById(id)
|
||||||
|
|
||||||
if (!$(audio).data('disabled')) {
|
if (audio && !$(audio).data('disabled')) {
|
||||||
audio.play()
|
audio.play()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -265,7 +267,9 @@ async function onTtsVoicesClick() {
|
|||||||
<b class="voice_name">${voice.name}</b>
|
<b class="voice_name">${voice.name}</b>
|
||||||
<i onclick="tts_preview('${voice.voice_id}')" class="fa-solid fa-play"></i>
|
<i onclick="tts_preview('${voice.voice_id}')" class="fa-solid fa-play"></i>
|
||||||
</div>`
|
</div>`
|
||||||
popupText += `<audio id="${voice.voice_id}" src="${voice.preview_url}" data-disabled="${voice.preview_url == false}"></audio>`
|
if (voice.preview_url) {
|
||||||
|
popupText += `<audio id="${voice.voice_id}" src="${voice.preview_url}" data-disabled="${voice.preview_url == false}"></audio>`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
popupText = 'Could not load voices list. Check your API key.'
|
popupText = 'Could not load voices list. Check your API key.'
|
||||||
@@ -327,7 +331,7 @@ function completeCurrentAudioJob() {
|
|||||||
*/
|
*/
|
||||||
async function addAudioJob(response) {
|
async function addAudioJob(response) {
|
||||||
const audioData = await response.blob()
|
const audioData = await response.blob()
|
||||||
if (!audioData.type in ['audio/mpeg', 'audio/wav', 'audio/x-wav', 'audio/wave']) {
|
if (!audioData.type in ['audio/mpeg', 'audio/wav', 'audio/x-wav', 'audio/wave', 'audio/webm']) {
|
||||||
throw `TTS received HTTP response with invalid data format. Expecting audio/mpeg, got ${audioData.type}`
|
throw `TTS received HTTP response with invalid data format. Expecting audio/mpeg, got ${audioData.type}`
|
||||||
}
|
}
|
||||||
audioJobQueue.push(audioData)
|
audioJobQueue.push(audioData)
|
||||||
@@ -414,6 +418,7 @@ async function processTtsQueue() {
|
|||||||
const voice = await ttsProvider.getVoice((voiceMap[char]))
|
const voice = await ttsProvider.getVoice((voiceMap[char]))
|
||||||
const voiceId = voice.voice_id
|
const voiceId = voice.voice_id
|
||||||
if (voiceId == null) {
|
if (voiceId == null) {
|
||||||
|
toastr.error(`Specified voice for ${char} was not found. Check the TTS extension settings.`)
|
||||||
throw `Unable to attain voiceId for ${char}`
|
throw `Unable to attain voiceId for ${char}`
|
||||||
}
|
}
|
||||||
tts(text, voiceId)
|
tts(text, voiceId)
|
||||||
@@ -494,7 +499,6 @@ async function voicemapIsValid(parsedVoiceMap) {
|
|||||||
|
|
||||||
async function updateVoiceMap() {
|
async function updateVoiceMap() {
|
||||||
let isValidResult = false
|
let isValidResult = false
|
||||||
const context = getContext()
|
|
||||||
|
|
||||||
const value = $('#tts_voice_map').val()
|
const value = $('#tts_voice_map').val()
|
||||||
const parsedVoiceMap = parseVoiceMap(value)
|
const parsedVoiceMap = parseVoiceMap(value)
|
||||||
|
130
public/scripts/extensions/tts/novel.js
Normal file
130
public/scripts/extensions/tts/novel.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { getRequestHeaders } from "../../../script.js"
|
||||||
|
import { getPreviewString } from "./index.js"
|
||||||
|
|
||||||
|
export { NovelTtsProvider }
|
||||||
|
|
||||||
|
class NovelTtsProvider {
|
||||||
|
//########//
|
||||||
|
// Config //
|
||||||
|
//########//
|
||||||
|
|
||||||
|
settings
|
||||||
|
voices = []
|
||||||
|
separator = ' . '
|
||||||
|
audioElement = document.createElement('audio')
|
||||||
|
|
||||||
|
defaultSettings = {
|
||||||
|
voiceMap: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
get settingsHtml() {
|
||||||
|
let html = `Use NovelAI's TTS engine.<br>
|
||||||
|
The Voice IDs in the preview list are only examples, as it can be any string of text. Feel free to try different options!<br>
|
||||||
|
<small><i>Hint: Save an API key in the NovelAI API settings to use it here.</i></small>`;
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSettingsChange() {
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info("Settings loaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async onApplyClick() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//#################//
|
||||||
|
// TTS Interfaces //
|
||||||
|
//#################//
|
||||||
|
|
||||||
|
async getVoice(voiceName) {
|
||||||
|
if (!voiceName) {
|
||||||
|
throw `TTS Voice name not provided`
|
||||||
|
}
|
||||||
|
|
||||||
|
return { name: voiceName, voice_id: voiceName, lang: 'en-US', preview_url: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateTts(text, voiceId) {
|
||||||
|
const response = await this.fetchTtsGeneration(text, voiceId)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
//###########//
|
||||||
|
// API CALLS //
|
||||||
|
//###########//
|
||||||
|
async fetchTtsVoiceIds() {
|
||||||
|
const voices = [
|
||||||
|
{ name: 'Ligeia', voice_id: 'Ligeia', lang: 'en-US', preview_url: false },
|
||||||
|
{ name: 'Aini', voice_id: 'Aini', lang: 'en-US', preview_url: false },
|
||||||
|
{ name: 'Orea', voice_id: 'Orea', lang: 'en-US', preview_url: false },
|
||||||
|
{ name: 'Claea', voice_id: 'Claea', lang: 'en-US', preview_url: false },
|
||||||
|
{ name: 'Lim', voice_id: 'Lim', lang: 'en-US', preview_url: false },
|
||||||
|
{ name: 'Aurae', voice_id: 'Aurae', lang: 'en-US', preview_url: false },
|
||||||
|
{ name: 'Naia', voice_id: 'Naia', lang: 'en-US', preview_url: false },
|
||||||
|
{ name: 'Aulon', voice_id: 'Aulon', lang: 'en-US', preview_url: false },
|
||||||
|
{ name: 'Elei', voice_id: 'Elei', lang: 'en-US', preview_url: false },
|
||||||
|
{ name: 'Ogma', voice_id: 'Ogma', lang: 'en-US', preview_url: false },
|
||||||
|
{ name: 'Raid', voice_id: 'Raid', lang: 'en-US', preview_url: false },
|
||||||
|
{ name: 'Pega', voice_id: 'Pega', lang: 'en-US', preview_url: false },
|
||||||
|
{ name: 'Lam', voice_id: 'Lam', lang: 'en-US', preview_url: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
return voices;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async previewTtsVoice(id) {
|
||||||
|
this.audioElement.pause();
|
||||||
|
this.audioElement.currentTime = 0;
|
||||||
|
|
||||||
|
const text = getPreviewString('en-US')
|
||||||
|
const response = await this.fetchTtsGeneration(text, id)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const audio = await response.blob();
|
||||||
|
const url = URL.createObjectURL(audio);
|
||||||
|
this.audioElement.src = url;
|
||||||
|
this.audioElement.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchTtsGeneration(inputText, voiceId) {
|
||||||
|
console.info(`Generating new TTS for voice_id ${voiceId}`)
|
||||||
|
const response = await fetch(`/novel_tts`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: getRequestHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
|
"text": inputText,
|
||||||
|
"voice": voiceId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (!response.ok) {
|
||||||
|
toastr.error(response.statusText, 'TTS Generation Failed');
|
||||||
|
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
}
|
@@ -118,7 +118,8 @@ class SileroTtsProvider {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
|
toastr.error(response.statusText, 'TTS Generation Failed');
|
||||||
|
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
@@ -60,6 +60,8 @@ const tokenizers = {
|
|||||||
GPT3: 1,
|
GPT3: 1,
|
||||||
CLASSIC: 2,
|
CLASSIC: 2,
|
||||||
LLAMA: 3,
|
LLAMA: 3,
|
||||||
|
NERD: 4,
|
||||||
|
NERD2: 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
const send_on_enter_options = {
|
const send_on_enter_options = {
|
||||||
|
98
server.js
98
server.js
@@ -6,7 +6,7 @@ const { hideBin } = require('yargs/helpers');
|
|||||||
const net = require("net");
|
const net = require("net");
|
||||||
// work around a node v20 bug: https://github.com/nodejs/node/issues/47822#issuecomment-1564708870
|
// work around a node v20 bug: https://github.com/nodejs/node/issues/47822#issuecomment-1564708870
|
||||||
if (net.setDefaultAutoSelectFamily) {
|
if (net.setDefaultAutoSelectFamily) {
|
||||||
net.setDefaultAutoSelectFamily(false);
|
net.setDefaultAutoSelectFamily(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cliArguments = yargs(hideBin(process.argv))
|
const cliArguments = yargs(hideBin(process.argv))
|
||||||
@@ -128,23 +128,25 @@ const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|||||||
|
|
||||||
const { SentencePieceProcessor, cleanText } = require("sentencepiece-js");
|
const { SentencePieceProcessor, cleanText } = require("sentencepiece-js");
|
||||||
|
|
||||||
let spp;
|
let spp_llama;
|
||||||
|
let spp_nerd;
|
||||||
|
let spp_nerd_v2;
|
||||||
|
|
||||||
async function loadSentencepieceTokenizer() {
|
async function loadSentencepieceTokenizer(modelPath) {
|
||||||
try {
|
try {
|
||||||
const spp = new SentencePieceProcessor();
|
const spp = new SentencePieceProcessor();
|
||||||
await spp.load("src/sentencepiece/tokenizer.model");
|
await spp.load(modelPath);
|
||||||
return spp;
|
return spp;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Sentencepiece tokenizer failed to load.");
|
console.error("Sentencepiece tokenizer failed to load: " + modelPath, error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function countTokensLlama(text) {
|
async function countSentencepieceTokens(spp, text) {
|
||||||
// Fallback to strlen estimation
|
// Fallback to strlen estimation
|
||||||
if (!spp) {
|
if (!spp) {
|
||||||
return Math.ceil(v.length / 3.35);
|
return Math.ceil(text.length / 3.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
let cleaned = cleanText(text);
|
let cleaned = cleanText(text);
|
||||||
@@ -2795,14 +2797,22 @@ app.post("/savepreset_openai", jsonParser, function (request, response) {
|
|||||||
return response.send({ name });
|
return response.send({ name });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/tokenize_llama", jsonParser, async function (request, response) {
|
function createTokenizationHandler(getTokenizerFn) {
|
||||||
if (!request.body) {
|
return async function (request, response) {
|
||||||
return response.sendStatus(400);
|
if (!request.body) {
|
||||||
}
|
return response.sendStatus(400);
|
||||||
|
}
|
||||||
|
|
||||||
const count = await countTokensLlama(request.body.text);
|
const text = request.body.text || '';
|
||||||
return response.send({ count });
|
const tokenizer = getTokenizerFn();
|
||||||
});
|
const count = await countSentencepieceTokens(tokenizer, text);
|
||||||
|
return response.send({ count });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
app.post("/tokenize_llama", jsonParser, createTokenizationHandler(() => spp_llama));
|
||||||
|
app.post("/tokenize_nerdstash", jsonParser, createTokenizationHandler(() => spp_nerd));
|
||||||
|
app.post("/tokenize_nerdstash_v2", jsonParser, createTokenizationHandler(() => spp_nerd_v2));
|
||||||
|
|
||||||
// ** REST CLIENT ASYNC WRAPPERS **
|
// ** REST CLIENT ASYNC WRAPPERS **
|
||||||
|
|
||||||
@@ -2861,7 +2871,11 @@ const setupTasks = async function () {
|
|||||||
// Colab users could run the embedded tool
|
// Colab users could run the embedded tool
|
||||||
if (!is_colab) await convertWebp();
|
if (!is_colab) await convertWebp();
|
||||||
|
|
||||||
spp = await loadSentencepieceTokenizer();
|
[spp_llama, spp_nerd, spp_nerd_v2] = await Promise.all([
|
||||||
|
loadSentencepieceTokenizer('src/sentencepiece/tokenizer.model'),
|
||||||
|
loadSentencepieceTokenizer('src/sentencepiece/nerdstash.model'),
|
||||||
|
loadSentencepieceTokenizer('src/sentencepiece/nerdstash_v2.model'),
|
||||||
|
]);
|
||||||
|
|
||||||
console.log('Launching...');
|
console.log('Launching...');
|
||||||
|
|
||||||
@@ -3197,6 +3211,40 @@ app.post('/google_translate', jsonParser, async (request, response) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/novel_tts', jsonParser, async (request, response) => {
|
||||||
|
const token = readSecret(SECRET_KEYS.NOVEL);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return response.sendStatus(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = request.body.text;
|
||||||
|
const voice = request.body.voice;
|
||||||
|
|
||||||
|
if (!text || !voice) {
|
||||||
|
return response.sendStatus(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fetch = require('node-fetch').default;
|
||||||
|
const url = `${api_novelai}/ai/generate-voice?text=${encodeURIComponent(text)}&voice=-1&seed=${encodeURIComponent(voice)}&opus=false&version=v2`;
|
||||||
|
const result = await fetch(url, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'audio/webm' } });
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
return response.sendStatus(result.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks = await readAllChunks(result.body);
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
response.setHeader('Content-Type', 'audio/webm');
|
||||||
|
return response.send(buffer);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return response.sendStatus(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/delete_sprite', jsonParser, async (request, response) => {
|
app.post('/delete_sprite', jsonParser, async (request, response) => {
|
||||||
const label = request.body.label;
|
const label = request.body.label;
|
||||||
const name = request.body.name;
|
const name = request.body.name;
|
||||||
@@ -3343,6 +3391,26 @@ function readSecret(key) {
|
|||||||
return secrets[key];
|
return secrets[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function readAllChunks(readableStream) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Consume the readable stream
|
||||||
|
const chunks = [];
|
||||||
|
readableStream.on('data', (chunk) => {
|
||||||
|
chunks.push(chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
readableStream.on('end', () => {
|
||||||
|
console.log('Finished reading the stream.');
|
||||||
|
resolve(chunks);
|
||||||
|
});
|
||||||
|
|
||||||
|
readableStream.on('error', (error) => {
|
||||||
|
console.error('Error while reading the stream:', error);
|
||||||
|
reject();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function getImageBuffers(zipFilePath) {
|
async function getImageBuffers(zipFilePath) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Check if the zip file exists
|
// Check if the zip file exists
|
||||||
|
BIN
src/sentencepiece/nerdstash.model
Normal file
BIN
src/sentencepiece/nerdstash.model
Normal file
Binary file not shown.
BIN
src/sentencepiece/nerdstash_v2.model
Normal file
BIN
src/sentencepiece/nerdstash_v2.model
Normal file
Binary file not shown.
Reference in New Issue
Block a user