From 98db02da10aeb6bfdfc6ca6a5736def077205033 Mon Sep 17 00:00:00 2001 From: Rhystic1 Date: Mon, 8 Dec 2025 17:13:33 +0000 Subject: [PATCH 01/16] Add support for GPT-SoVits TTS provider in ChatInterface.vue and configure Vite proxy --- src/components/views/ChatInterface.vue | 196 ++++++++++++++++++++++++- vite.config.ts | 5 + 2 files changed, 197 insertions(+), 4 deletions(-) diff --git a/src/components/views/ChatInterface.vue b/src/components/views/ChatInterface.vue index ef961d9..33bc2a1 100644 --- a/src/components/views/ChatInterface.vue +++ b/src/components/views/ChatInterface.vue @@ -190,18 +190,45 @@
- Enables TTS using a local AllTalk instance.
- Requires AllTalk running with XTTSv2.

- Character voices must be in the AllTalk voices folder (e.g. "anis.wav"). + Enables TTS using a local TTS server.
+ Supports AllTalk (XTTSv2) or GPT-SoVits.

+ Character voices must be in the appropriate voices folder.
- + + + + + + + + + + + + + + @@ -307,6 +334,10 @@ const mode = ref('roleplay') const playbackMode = ref('auto') const ttsEnabled = ref(false) const ttsEndpoint = ref('http://localhost:7851') +const ttsProvider = ref<'alltalk' | 'gptsovits'>('alltalk') +const gptSovitsEndpoint = ref('http://localhost:9880') +const gptSovitsBasePath = ref('C:/GPT-SoVITS') +const gptSovitsPromptTextCache = new Map() const userInput = ref('') const isLoading = ref(false) const isStopped = ref(false) @@ -338,6 +369,11 @@ const providerOptions = [ { label: 'OpenRouter', value: 'openrouter' } ] +const ttsProviderOptions = [ + { label: 'AllTalk (XTTSv2)', value: 'alltalk' }, + { label: 'GPT-SoVits', value: 'gptsovits' } +] + const modelOptions = computed(() => { if (apiProvider.value === 'perplexity') { return [ @@ -393,6 +429,18 @@ watch(ttsEndpoint, (newVal) => { localStorage.setItem('nikke_tts_endpoint', newVal) }) +watch(ttsProvider, (newVal) => { + localStorage.setItem('nikke_tts_provider', newVal) +}) + +watch(gptSovitsEndpoint, (newVal) => { + localStorage.setItem('nikke_gptsovits_endpoint', newVal) +}) + +watch(gptSovitsBasePath, (newVal) => { + localStorage.setItem('nikke_gptsovits_basepath', newVal) +}) + watch(model, (newVal) => { if (newVal) localStorage.setItem('nikke_model', newVal) }) @@ -482,6 +530,15 @@ const initializeSettings = async () => { const savedTtsEndpoint = localStorage.getItem('nikke_tts_endpoint') if (savedTtsEndpoint) ttsEndpoint.value = savedTtsEndpoint + const savedTtsProvider = localStorage.getItem('nikke_tts_provider') + if (savedTtsProvider === 'alltalk' || savedTtsProvider === 'gptsovits') ttsProvider.value = savedTtsProvider + + const savedGptSovitsEndpoint = localStorage.getItem('nikke_gptsovits_endpoint') + if (savedGptSovitsEndpoint) gptSovitsEndpoint.value = savedGptSovitsEndpoint + + const savedGptSovitsBasePath = localStorage.getItem('nikke_gptsovits_basepath') + if (savedGptSovitsBasePath) gptSovitsBasePath.value = savedGptSovitsBasePath + const savedTokenUsage = localStorage.getItem('nikke_token_usage') if (savedTokenUsage && tokenUsageOptions.some((t) => t.value === savedTokenUsage)) tokenUsage.value = savedTokenUsage @@ -572,6 +629,9 @@ const saveSession = () => { yapEnabled: market.live2d.yapEnabled, ttsEnabled: ttsEnabled.value, ttsEndpoint: ttsEndpoint.value, + ttsProvider: ttsProvider.value, + gptSovitsEndpoint: gptSovitsEndpoint.value, + gptSovitsBasePath: gptSovitsBasePath.value, tokenUsage: tokenUsage.value } } @@ -640,6 +700,18 @@ const handleFileUpload = (event: Event) => { ttsEndpoint.value = data.settings.ttsEndpoint } + if (data.settings.ttsProvider === 'alltalk' || data.settings.ttsProvider === 'gptsovits') { + ttsProvider.value = data.settings.ttsProvider + } + + if (data.settings.gptSovitsEndpoint) { + gptSovitsEndpoint.value = data.settings.gptSovitsEndpoint + } + + if (data.settings.gptSovitsBasePath) { + gptSovitsBasePath.value = data.settings.gptSovitsBasePath + } + if (data.settings.tokenUsage && tokenUsageOptions.some((t) => t.value === data.settings.tokenUsage)) { tokenUsage.value = data.settings.tokenUsage } @@ -2376,9 +2448,125 @@ const getCharacterName = (id: string): string | null => { return char ? char.name : id } +const playTTSGptSovits = async (text: string, characterName: string) => { + // Clean up character name to match folder/filename + // e.g. "Anis: Sparkling Summer" -> "anis_sparkling_summer" + const cleanName = characterName.toLowerCase().replace(/[^\w\s]/gi, '').replace(/\s+/g, '_') + + logDebug(`[TTS-GPTSoVits] Requesting TTS for ${characterName} (${cleanName})`) + + try { + let baseUrl = gptSovitsEndpoint.value + baseUrl = baseUrl.replace(/\/$/, '') + + // Handle CORS in dev mode by using Vite proxy + if (import.meta.env.DEV && (baseUrl.includes('localhost:9880') || baseUrl.includes('127.0.0.1:9880'))) { + baseUrl = '/gptsovits' + } + + // Construct paths for reference audio and prompt text + // User must place files at: {basePath}/GPT_SoVITS/voices/{character}/{character}.wav + // And prompt text at: {basePath}/GPT_SoVITS/voices/{character}/{character}.txt + // Clean up the base path: remove trailing slashes/backslashes, normalize to forward slashes + const basePath = gptSovitsBasePath.value + .replace(/[\\/]+$/, '') // Remove trailing slashes (both / and \) + .replace(/\\/g, '/') // Convert backslashes to forward slashes + const refAudioPath = `${basePath}/GPT_SoVITS/voices/${cleanName}/${cleanName}.wav` + const promptTextPath = `${basePath}/GPT_SoVITS/voices/${cleanName}/${cleanName}.txt` + + // Fetch prompt text from cache or API + let promptText = gptSovitsPromptTextCache.get(cleanName) + if (!promptText) { + try { + const promptResponse = await fetch(`${baseUrl}/read_prompt_text?path=${encodeURIComponent(promptTextPath)}`) + if (promptResponse.ok) { + const promptData = await promptResponse.json() + promptText = promptData.text || '' + if (promptText) { + gptSovitsPromptTextCache.set(cleanName, promptText) + logDebug(`[TTS-GPTSoVits] Loaded prompt text for ${cleanName}: "${promptText}"`) + } + } else { + console.warn(`[TTS-GPTSoVits] Could not fetch prompt text for ${cleanName}`) + promptText = '' + } + } catch (e) { + console.warn(`[TTS-GPTSoVits] Error fetching prompt text for ${cleanName}:`, e) + promptText = '' + } + } + + // Call the TTS endpoint + const payload = { + text: text, + text_lang: 'en', + text_split_method: 'cut0', + ref_audio_path: refAudioPath, + prompt_text: promptText, + prompt_lang: 'en', + media_type: 'wav', + streaming_mode: false, + // Quality parameters + top_k: 10, + top_p: 0.8, + temperature: 0.8, + speed_factor: 1.0 + } + + logDebug(`[TTS-GPTSoVits] Calling ${baseUrl}/tts with payload:`, payload) + + const response = await fetch(`${baseUrl}/tts`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload) + }) + + if (!response.ok) { + const errText = await response.text() + console.warn(`[TTS-GPTSoVits] API Error: ${response.status} - ${errText}`) + return + } + + // Response is audio blob + const audioBlob = await response.blob() + const audioUrl = URL.createObjectURL(audioBlob) + const audio = new Audio(audioUrl) + audio.volume = 1.0 + + // Sync yapping with audio + audio.onplay = () => { + market.live2d.isYapping = true + } + audio.onended = () => { + market.live2d.isYapping = false + URL.revokeObjectURL(audioUrl) // Clean up + } + audio.onerror = () => { + market.live2d.isYapping = false + URL.revokeObjectURL(audioUrl) + } + + audio.play().catch(e => { + console.warn('[TTS-GPTSoVits] Playback failed:', e) + market.live2d.isYapping = false + URL.revokeObjectURL(audioUrl) + }) + } catch (e) { + console.warn('[TTS-GPTSoVits] Error:', e) + } +} + const playTTS = async (text: string, characterName: string) => { if (!ttsEnabled.value || !characterName) return + // Dispatch to appropriate TTS provider + if (ttsProvider.value === 'gptsovits') { + return playTTSGptSovits(text, characterName) + } + + // AllTalk implementation (default) // Clean up character name to match filename // Remove special chars, replace spaces with underscores // e.g. "Anis: Sparkling Summer" -> "anis_sparkling_summer.wav" diff --git a/vite.config.ts b/vite.config.ts index 1867a81..76c8893 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -18,6 +18,11 @@ export default defineConfig({ target: 'http://127.0.0.1:7851', changeOrigin: true, rewrite: (path) => path.replace(/^\/alltalk/, '') + }, + '/gptsovits': { + target: 'http://127.0.0.1:9880', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/gptsovits/, '') } } } From 5a44c40772ff65fb197b0898f9c96ae626343006 Mon Sep 17 00:00:00 2001 From: Rhystic1 Date: Tue, 9 Dec 2025 21:13:07 +0000 Subject: [PATCH 02/16] - Created an internal JSON database for character profiles so that the user may use this instead of the standard web search. This is enabled by default and will be the recommended setting from now on, as it decreases costs and significantly improves speed. Currently contains 62 characters. Will be expanded in the near future. - Added the ability to optionally fallback to web search for characters that are not yet in the internal database if the internal DB setting is enabled. Strongly recommended to turn on, though this may incur the web search costs. - Enabled context caching for supported models like Gemini and Anthropic through OpenRouter. Experimental for now. Enabled by default as there is no real reason to disable it. Mostly relevant for users that select "High" or "Goddess" mode. - Improved chat window so that the user is more aware of what the model is doing, if the chat is being processed, or if there are more dialogue lines in the turn. - Refactored ChatInterface by moving prompts to a separate JSON file. --- src/components/views/ChatInterface.vue | 524 ++++++++++++-------- src/utils/json/characterProfiles.json | 639 +++++++++++++++++++++++++ src/utils/json/loadingMessages.json | 17 + src/utils/json/prompts.json | 37 ++ 4 files changed, 1030 insertions(+), 187 deletions(-) create mode 100644 src/utils/json/characterProfiles.json create mode 100644 src/utils/json/loadingMessages.json create mode 100644 src/utils/json/prompts.json diff --git a/src/components/views/ChatInterface.vue b/src/components/views/ChatInterface.vue index 33bc2a1..60928b3 100644 --- a/src/components/views/ChatInterface.vue +++ b/src/components/views/ChatInterface.vue @@ -23,7 +23,12 @@
-
...
+
+ + + {{ loadingStatus }} + +
@@ -83,13 +88,13 @@
- Your API key is stored locally in your browser's local storage, and it is never sent to Nikke-DB or any other server except the API provider when making requests. + Your API key is stored locally in your browser's local storage, and it is never sent to Nikke-DB. Users are responsible for any possible cost using this functionality. - Web search may incur additional costs, even with free models. Please consult your API provider's pricing documentation. + Web search may incur additional costs. Enable 'Use Nikke-DB Knowledge' to reduce reliance on web search.

In order to ensure a better quality experience, the model will search the Goddess of Victory: NIKKE Wikia to gather certain details regarding the characters that are part of the scene, such as how they address the Commander, their personality, etc.

-

Web search is used on the first turn and when new characters are introduced. The system minimizes searches to reduce costs.

+

Web search is used on the first turn and when new characters are introduced. You can disable this by enabling "Use Nikke-DB Knowledge".

It is strongly suggested to check your provider's documentation and model page for information regarding possible costs.

It is also recommended to select a limit on your API key to prevent unexpected charges.

@@ -108,6 +113,42 @@ + + + + + + + + + +