diff --git a/package.json b/package.json index c55db78c..3d00d790 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "gpt3-tokenizer": "^1.1.5", "html-to-image": "^1.11.11", "isomorphic-dompurify": "^1.8.0", + "jszip": "^3.10.1", "libsodium-wrappers-sumo": "^0.7.11", "localforage": "^1.10.0", "lodash": "^4.17.21", @@ -91,4 +92,4 @@ "vite-plugin-top-level-await": "^1.3.1", "vite-plugin-wasm": "^3.2.2" } -} \ No newline at end of file +} diff --git a/server.sh b/server.sh old mode 100755 new mode 100644 diff --git a/server/node/server.cjs b/server/node/server.cjs index faf1aa48..46d41d10 100644 --- a/server/node/server.cjs +++ b/server/node/server.cjs @@ -61,7 +61,6 @@ const reverseProxyFunc = async (req, res, next) => { headers: header, body: JSON.stringify(req.body) }); - // get response body as stream const originalBody = originalResponse.body; // get response headers diff --git a/src/lib/Setting/Pages/OtherBotSettings.svelte b/src/lib/Setting/Pages/OtherBotSettings.svelte index 7e1e2e89..d098445a 100644 --- a/src/lib/Setting/Pages/OtherBotSettings.svelte +++ b/src/lib/Setting/Pages/OtherBotSettings.svelte @@ -2,12 +2,16 @@ import Check from "src/lib/UI/GUI/CheckInput.svelte"; import { language } from "src/lang"; import Help from "src/lib/Others/Help.svelte"; + import { selectSingleFile } from "src/ts/util"; import { DataBase } from "src/ts/storage/database"; import { isTauri } from "src/ts/storage/globalApi"; import NumberInput from "src/lib/UI/GUI/NumberInput.svelte"; import TextInput from "src/lib/UI/GUI/TextInput.svelte"; import SelectInput from "src/lib/UI/GUI/SelectInput.svelte"; import OptionInput from "src/lib/UI/GUI/OptionInput.svelte"; + import SliderInput from "src/lib/UI/GUI/SliderInput.svelte"; + import Button from "src/lib/UI/GUI/Button.svelte"; + import { convertToBase64 } from "src/ts/process/uinttobase64";

{language.otherBots}

@@ -18,6 +22,7 @@ None Stable Diffusion WebUI + Novel AI @@ -56,6 +61,65 @@ {/if} {/if} +{#if $DataBase.sdProvider === 'novelai'} + Novel AI {language.providerURL} + + API Key + + + Model + + + Enable I2I + + + + {#if $DataBase.NAII2I} + + strength + + {$DataBase.NAIImgConfig.strength} + noise + + {$DataBase.NAIImgConfig.noise} + + base image + + If empty, a profile picture is sent. + + + {/if} + + Width + + Height + + Sampler + + steps + + CFG scale + + + {#if !$DataBase.NAII2I} + Use SMEA + + Use DYN + + {/if} +{/if} TTS ElevenLabs API key @@ -64,6 +128,9 @@ VOICEVOX URL +NovelAI API key + + {language.emotionImage} {language.emotionMethod} diff --git a/src/lib/SideBars/CharConfig.svelte b/src/lib/SideBars/CharConfig.svelte index 51d05dfe..5092502d 100644 --- a/src/lib/SideBars/CharConfig.svelte +++ b/src/lib/SideBars/CharConfig.svelte @@ -15,7 +15,7 @@ import Help from "../Others/Help.svelte"; import RegexData from "./Scripts/RegexData.svelte"; import { exportChar, shareRisuHub } from "src/ts/characterCards"; - import { getElevenTTSVoices, getWebSpeechTTSVoices, getVOICEVOXVoices, oaiVoices } from "src/ts/process/tts"; + import { getElevenTTSVoices, getWebSpeechTTSVoices, getVOICEVOXVoices, oaiVoices, getNovelAIVoices, FixNAITTS } from "src/ts/process/tts"; import { checkCharOrder, getFileSrc } from "src/ts/storage/globalApi"; import { addGroupChar, rmCharFromGroup } from "src/ts/process/group"; import RealmUpload from "../UI/Realm/RealmUpload.svelte"; @@ -27,6 +27,7 @@ import OptionInput from "../UI/GUI/OptionInput.svelte"; import RegexList from "./Scripts/RegexList.svelte"; import TriggerList from "./Scripts/TriggerList.svelte"; + let subMenu = 0 let openHubUpload = false @@ -130,12 +131,20 @@ } } } + } onDestroy(unsub); $:licensed = (currentChar.type === 'character') ? currentChar.data.license : '' + $: if (currentChar.data.ttsMode === 'novelai' && (currentChar.data as character).naittsConfig === undefined) { + (currentChar.data as character).naittsConfig = { + customvoice: false, + voice: 'Aini', + version: 'v2' + }; + } {#if licensed !== 'private'} @@ -542,6 +551,7 @@ Web Speech VOICEVOX OpenAI + NovelAI @@ -600,6 +610,31 @@ Intonation scale To use VOICEVOX, you need to run a colab and put the localtunnel URL in "Settings → Other Bots". https://colab.research.google.com/drive/1tyeXJSklNfjW-aZJAib1JfgOMFarAwze + {:else if currentChar.data.ttsMode === 'novelai'} + Custom Voice Seed + + {#if !currentChar.data.naittsConfig.customvoice} + Voice + + {#await getNovelAIVoices() then voices} + {#each voices as voiceGroup} + + {#each voiceGroup.voices as voice} + {voice} + {/each} + + {/each} + {/await} + + {:else} + Voice + + {/if} + Version + + v1 + v2 + {/if} {#if currentChar.data.ttsMode === 'openai'} OpenAI TTS uses your OpenAI key on the chat model section @@ -610,7 +645,7 @@ {/each} {/if} - {#if currentChar.data.ttsMode === 'webspeech' || currentChar.data.ttsMode === 'elevenlab' || currentChar.data.ttsMode === 'VOICEVOX'} + {#if currentChar.data.ttsMode === 'webspeech' || currentChar.data.ttsMode === 'elevenlab' || currentChar.data.ttsMode === 'VOICEVOX' || currentChar.data.ttsMode === 'novelai'}
diff --git a/src/ts/process/generateSeed.ts b/src/ts/process/generateSeed.ts new file mode 100644 index 00000000..670cbc98 --- /dev/null +++ b/src/ts/process/generateSeed.ts @@ -0,0 +1,9 @@ +export function generateRandomSeed(length) { + let result = ''; + const characters = '0123456789'; + const charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +} \ No newline at end of file diff --git a/src/ts/process/processzip.ts b/src/ts/process/processzip.ts new file mode 100644 index 00000000..6ef98fb9 --- /dev/null +++ b/src/ts/process/processzip.ts @@ -0,0 +1,15 @@ +import JSZip from "jszip"; + +export async function processZip(dataArray: Uint8Array): Promise { + const blob = new Blob([dataArray], { type: "application/zip" }); + const zip = new JSZip(); + const zipData = await zip.loadAsync(blob); + + const imageFile = Object.keys(zipData.files).find(fileName => /\.(jpg|jpeg|png)$/.test(fileName)); + if (imageFile) { + const imageData = await zipData.files[imageFile].async("base64"); + return `data:image/png;base64,${imageData}`; + } else { + throw new Error("No image found in ZIP file"); + } +} \ No newline at end of file diff --git a/src/ts/process/stableDiff.ts b/src/ts/process/stableDiff.ts index bcf11ffb..262fbb14 100644 --- a/src/ts/process/stableDiff.ts +++ b/src/ts/process/stableDiff.ts @@ -2,11 +2,13 @@ import { get } from "svelte/store" import { DataBase, type character } from "../storage/database" import { requestChatData } from "./request" import { alertError } from "../alert" -import { globalFetch } from "../storage/globalApi" +import { globalFetch, readImage } from "../storage/globalApi" import { CharEmotion } from "../stores" import type { OpenAIChat } from "." - - +import { processZip } from "./processzip" +import { convertToBase64 } from "./uinttobase64" +import type { List } from "lodash" +import { generateRandomSeed } from "./generateSeed" export async function stableDiff(currentChar:character,prompt:string){ const mainPrompt = "assistant is a chat analyzer.\nuser will input a data of situation with key and values before chat, and a chat of a user and character.\nView the status of the chat and change the data.\nif data's key starts with $, it must change it every time.\nif data value is none, it must change it." let db = get(DataBase) @@ -129,7 +131,7 @@ export async function stableDiff(currentChar:character,prompt:string){ "cfg_scale": db.sdCFG, "prompt": prompts.join(','), "negative_prompt": neg, - 'sampler_name': db.sdConfig.sampler_name, + "sampler_name": db.sdConfig.sampler_name, "enable_hr": db.sdConfig.enable_hr, "denoising_strength": db.sdConfig.denoising_strength, "hr_scale": db.sdConfig.hr_scale, @@ -161,6 +163,113 @@ export async function stableDiff(currentChar:character,prompt:string){ return false } } + if(db.sdProvider === 'novelai'){ + let prompts:string[] = [] + let neg = '' + for(let i=0;i { + const sourceNode = audioContext.createBufferSource(); + sourceNode.buffer = decodedData; + sourceNode.connect(audioContext.destination); + sourceNode.start(); + }); + } else { + alertError("Error fetching or decoding audio data"); + } + break; + } } - } export const oaiVoices = [ @@ -189,4 +210,29 @@ export async function getVOICEVOXVoices() { }) speakersInfo.unshift({ name: "None", list: null}) return speakersInfo; +} + +export async function getNovelAIVoices(){ + return [ + { + gender: "UNISEX", + voices: ['Anananan'] + }, + { + gender: "FEMALE", + voices: ['Aini', 'Orea', 'Claea', 'Lim', 'Aurae', 'Naia'] + }, + { + gender: "MALE", + voices: ['Aulon', 'Elei', 'Ogma', 'Raid', 'Pega', 'Lam'] + } + ]; +} + +export async function FixNAITTS(data:character){ + if (data.naittsConfig === undefined){ + data.naittsConfig.voice = 'Anananan' + } + + return data } \ No newline at end of file diff --git a/src/ts/process/uinttobase64.ts b/src/ts/process/uinttobase64.ts new file mode 100644 index 00000000..230d911a --- /dev/null +++ b/src/ts/process/uinttobase64.ts @@ -0,0 +1,17 @@ +export async function convertToBase64(data: Uint8Array): Promise { + return new Promise((resolve, reject) => { + const blob = new Blob([data]); + const reader = new FileReader(); + + reader.onloadend = function() { + const base64String = reader.result as string; + resolve(base64String); + }; + + reader.onerror = function(error) { + reject(error); + }; + + reader.readAsDataURL(blob); + }); +} \ No newline at end of file diff --git a/src/ts/storage/database.ts b/src/ts/storage/database.ts index 2da37b67..d3fe223f 100644 --- a/src/ts/storage/database.ts +++ b/src/ts/storage/database.ts @@ -174,6 +174,18 @@ export function setDatabase(data:Database){ if(checkNullish(data.sdCFG)){ data.sdCFG = 7 } + if(checkNullish(data.NAIImgUrl)){ + data.NAIImgUrl = 'https://api.novelai.net/ai/generate-image' + } + if(checkNullish(data.NAIApiKey)){ + data.NAIApiKey = '' + } + if(checkNullish(data.NAIImgModel)){ + data.NAIImgModel = 'nai-diffusion-3' + } + if(checkNullish(data.NAII2I)){ + data.NAII2I = true + } if(checkNullish(data.textTheme)){ data.textTheme = "standard" } @@ -231,6 +243,20 @@ export function setDatabase(data:Database){ hr_upscaler:"Latent" } } + if(checkNullish(data.NAIImgConfig)){ + data.NAIImgConfig = { + width:512, + height:768, + sampler:"k_dpmpp_sde", + steps:28, + scale:5, + sm:true, + sm_dyn:true, + noise:0.0, + strength:0.3, + image:"" + } + } if(checkNullish(data.customTextTheme)){ data.customTextTheme = { FontColorStandard: "#f8f8f2", @@ -394,6 +420,11 @@ export interface Database{ sdSteps:number sdCFG:number sdConfig:sdConfig + NAIImgUrl:string + NAIApiKey:string + NAIImgModel:string + NAII2I:boolean + NAIImgConfig:NAIImgConfig runpodKey:string promptPreprocess:boolean bias: [string, number][] @@ -588,6 +619,11 @@ export interface character{ INTONATION_SCALE?: number VOLUME_SCALE?: number } + naittsConfig?:{ + customvoice?: boolean + voice?: string + version?: string + } supaMemory?:boolean additionalAssets?:[string, string, string][] ttsReadOnlyQuoted?:boolean @@ -717,6 +753,18 @@ interface sdConfig{ hr_upscaler:string } +interface NAIImgConfig{ + width:number, + height:number, + sampler:string, + steps:number, + scale:number, + sm:boolean, + sm_dyn:boolean, + noise:number, + strength:number, + image:string +} export type FormatingOrderItem = 'main'|'jailbreak'|'chats'|'lorebook'|'globalNote'|'authorNote'|'lastChat'|'description'|'postEverything'|'personaPrompt' export interface Chat{ diff --git a/src/ts/storage/globalApi.ts b/src/ts/storage/globalApi.ts index 5ddae585..e7db8f9b 100644 --- a/src/ts/storage/globalApi.ts +++ b/src/ts/storage/globalApi.ts @@ -661,7 +661,7 @@ export async function globalFetch(url:string, arg:{ method: method, signal: arg.abortSignal }) - + addFetchLog("Uint8Array Response", da.ok && da.status >= 200 && da.status < 300) return { ok: da.ok && da.status >= 200 && da.status < 300, @@ -685,6 +685,16 @@ export async function globalFetch(url:string, arg:{ headers: headers, method: method }) + if(da.headers.get('content-type')?.includes('application/x-zip-compressed')){ + const daText = await da.blob() + + addFetchLog(daText, da.ok && da.status >= 200 && da.status < 300) + return { + ok: da.ok && da.status >= 200 && da.status < 300, + data: daText, + headers: Object.fromEntries(da.headers) + } + } const daText = await da.text() try { const dat = JSON.parse(daText)