{#key charas}
diff --git a/src/preload.ts b/src/preload.ts
index 53b9190e..456316f2 100644
--- a/src/preload.ts
+++ b/src/preload.ts
@@ -4,7 +4,7 @@ export function preLoadCheck(){
const searchParams = new URLSearchParams(location.search);
//@ts-ignore
- const isTauri = !!window.__TAURI__
+ const isTauri = !!window.__TAURI_INTERNALS__
//@ts-ignore
const isNodeServer = !!globalThis.__NODE__
const isCapacitor = Capacitor.isNativePlatform();
@@ -19,6 +19,15 @@ export function preLoadCheck(){
else if(searchParams.has('mainpage')) {
localStorage.setItem('mainpage', searchParams.get('mainpage'));
}
+
+ if(isWeb) {
+ //Add beforeunload event listener to prevent the user from leaving the page
+ window.addEventListener('beforeunload', (e) => {
+ e.preventDefault()
+ //legacy browser
+ e.returnValue = true
+ })
+ }
// Redirect to the main page if the user has not visited the main page
diff --git a/src/styles.css b/src/styles.css
index 97e87ca7..4a8eaab9 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -242,4 +242,16 @@ html, body{
background-color: var(--tw-prose-pre-bg);
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
+}
+
+.x-risu-risu-inlay-image{
+ @apply w-full flex justify-center
+}
+
+.x-risu-risu-inlay-image > img{
+ @apply rounded-lg focus:outline-none max-w-80 w-full
+}
+
+.z-100{
+ z-index: 100;
}
\ No newline at end of file
diff --git a/src/ts/alert.ts b/src/ts/alert.ts
index 25abb1d2..30049793 100644
--- a/src/ts/alert.ts
+++ b/src/ts/alert.ts
@@ -246,7 +246,7 @@ export async function alertCardExport(type:string = ''){
export async function alertTOS(){
- if(localStorage.getItem('tos') === 'true'){
+ if(localStorage.getItem('tos2') === 'true'){
return true
}
@@ -263,7 +263,7 @@ export async function alertTOS(){
}
if(get(alertStore).msg === 'yes'){
- localStorage.setItem('tos', 'true')
+ localStorage.setItem('tos2', 'true')
return true
}
diff --git a/src/ts/characterCards.ts b/src/ts/characterCards.ts
index ffe37d8e..dfe3deda 100644
--- a/src/ts/characterCards.ts
+++ b/src/ts/characterCards.ts
@@ -14,6 +14,9 @@ import { PngChunk } from "./pngChunk"
import type { OnnxModelFiles } from "./process/transformers"
import { CharXReader, CharXWriter } from "./process/processzip"
import { Capacitor } from "@capacitor/core"
+import { exportModule, readModule, type RisuModule } from "./process/modules"
+import { readFile } from "@tauri-apps/plugin-fs"
+import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
export const hubURL = "https://sv.risuai.xyz"
@@ -65,6 +68,9 @@ async function importCharacterProcess(f:{
}
}
+ let db = get(DataBase)
+ db.statics.imports += 1
+
if(f.name.endsWith('charx')){
console.log('reading charx')
alertStore.set({
@@ -80,11 +86,18 @@ async function importCharacterProcess(f:{
alertError(language.errors.noData)
return
}
- const card = JSON.parse(cardData)
+ const card:CharacterCardV3 = JSON.parse(cardData)
if(CCardLib.character.check(card) !== 'v3'){
alertError(language.errors.noData)
return
}
+ if(reader.moduleData){
+ const md = await readModule(Buffer.from(reader.moduleData))
+ card.data.extensions ??= {}
+ card.data.extensions.risuai ??= {}
+ card.data.extensions.risuai.triggerscript = md.trigger ?? []
+ card.data.extensions.risuai.customScripts = md.regex ?? []
+ }
await importCharacterCardSpec(card, undefined, 'normal', reader.assets)
let db = get(DataBase)
return db.characters.length - 1
@@ -133,7 +146,7 @@ async function importCharacterProcess(f:{
continue
}
if(chunk.key.startsWith('chara-ext-asset_')){
- const assetIndex = (chunk.key.replace('chara-ext-asset_', ''))
+ const assetIndex = chunk.key.replace('chara-ext-asset_:', '').replace('chara-ext-asset_', '')
alertWait('Loading... (Reading Asset ' + assetIndex + ')' )
const assetData = Buffer.from(chunk.value, 'base64')
const assetId = await saveAsset(assetData)
@@ -145,7 +158,7 @@ async function importCharacterProcess(f:{
return
}
- if(!readedChara){
+ if(readedCCv3){
readedChara = readedCCv3
}
@@ -226,7 +239,6 @@ async function importCharacterProcess(f:{
const charaData:OldTavernChar = JSON.parse(Buffer.from(readedChara, 'base64').toString('utf-8'))
console.log(charaData)
const imgp = await saveAsset(await reencodeImage(img))
- let db = get(DataBase)
db.characters.push(convertOffSpecCards(charaData, imgp))
DataBase.set(db)
alertNormal(language.importedCharacter)
@@ -289,6 +301,14 @@ export async function characterURLImport() {
const hash = location.hash
+ if(hash.startsWith('#import=')){
+ const url = hash.replace('#import=', '')
+ const res = await fetch(url, {
+ method: 'GET',
+ })
+ const data = new Uint8Array(await res.arrayBuffer())
+ importFile(getFileName(res), data)
+ }
if(hash.startsWith('#import_module=')){
const data = hash.replace('#import_module=', '')
const importData = JSON.parse(Buffer.from(decodeURIComponent(data), 'base64').toString('utf-8'))
@@ -304,7 +324,7 @@ export async function characterURLImport() {
db.modules.push(importData)
setDatabase(db)
alertNormal(language.successImport)
- SettingsMenuIndex.set(1)
+ SettingsMenuIndex.set(14)
settingsOpen.set(true)
return
}
@@ -315,10 +335,148 @@ export async function characterURLImport() {
name: 'imported.risupreset',
data: importData
})
- SettingsMenuIndex.set(14)
+ SettingsMenuIndex.set(1)
settingsOpen.set(true)
return
+ }
+ if(hash.startsWith('#share_character')){
+ const data = await fetch("/sw/share/character")
+ if(data.status !== 200){
+ return
+ }
+ const charx = new Uint8Array(await data.arrayBuffer())
+ await importCharacterProcess({
+ name: 'shared.charx',
+ data: charx
+ })
+ }
+ if(hash.startsWith('#share_module')){
+ const data = await fetch("/sw/share/module")
+ if(data.status !== 200){
+ return
+ }
+ const module = new Uint8Array(await data.arrayBuffer())
+ const md = await readModule(Buffer.from(module))
+ md.id = v4()
+ const db = get(DataBase)
+ db.modules.push(md)
+ setDatabase(db)
+ alertNormal(language.successImport)
+ SettingsMenuIndex.set(14)
+ settingsOpen.set(true)
+ }
+ if(hash.startsWith('#share_preset')){
+ const data = await fetch("/sw/share/preset")
+ if(data.status !== 200){
+ return
+ }
+ const preset = new Uint8Array(await data.arrayBuffer())
+ await importPreset({
+ name: 'shared.risup',
+ data: preset
+ })
+ SettingsMenuIndex.set(1)
+ settingsOpen.set(true)
+ }
+ if ("launchQueue" in window) {
+ const handleFiles = async (files:FileSystemFileHandle[]) => {
+ for(const f of files){
+ const file = await f.getFile()
+ const data = new Uint8Array(await file.arrayBuffer())
+ importFile(f.name, data);
+ }
+ }
+ //@ts-ignore
+ window.launchQueue.setConsumer((launchParams) => {
+ if (launchParams.files && launchParams.files.length) {
+ const files = launchParams.files as FileSystemFileHandle[]
+ handleFiles(files)
+ }
+ });
+ }
+
+ if("tauriOpenedFiles" in window){
+ //@ts-ignore
+ const files:string[] = window.tauriOpenedFiles
+ if(files){
+ for(const file of files){
+ const data = await readFile(file)
+ importFile(file, data)
+ }
+ }
+ }
+ if(isTauri){
+ await onOpenUrl((urls) => {
+ for(const url of urls){
+ const splited = url.split('/')
+ const id = splited[splited.length - 1]
+ const type = splited[splited.length - 2]
+ switch(type){
+ case 'realm':{
+ downloadRisuHub(id)
+ }
+ }
+ }
+ })
+ }
+
+ async function importFile(name:string, data:Uint8Array) {
+ if(name.endsWith('.charx') || name.endsWith('.png')){
+ await importCharacterProcess({
+ name: name,
+ data: data
+ })
+ return
+ }
+ if(name.endsWith('.risupreset') || name.endsWith('.risup')){
+ await importPreset({
+ name: name,
+ data: data
+ })
+ SettingsMenuIndex.set(1)
+ settingsOpen.set(true)
+ alertNormal(language.successImport)
+ return
+ }
+ if(name.endsWith('risum')){
+ const md = await readModule(Buffer.from(data))
+ md.id = v4()
+ const db = get(DataBase)
+ db.modules.push(md)
+ setDatabase(db)
+ alertNormal(language.successImport)
+ SettingsMenuIndex.set(14)
+ settingsOpen.set(true)
+ return
+ }
+ }
+
+ function getFileName(res : Response) : string {
+ return getFormContent(res.headers.get('content-disposition')) || getFromURL(res.url);
+
+ function getFormContent(contentDisposition : string) {
+ if (!contentDisposition) return null;
+ const pattern = /filename\*=UTF-8''([^"';\n]+)|filename[^;\n=]*=["']?([^"';\n]+)["']?/;
+ const matches = contentDisposition.match(pattern);
+ if (matches) {
+ if (matches[1]) {
+ return decodeURIComponent(matches[1]);
+ } else if (matches[2]) {
+ return matches[2];
+ }
+ }
+ return null;
+ }
+
+ function getFromURL(url : string) : string {
+ try {
+ const path = new URL(url).pathname;
+ return path.substring(path.lastIndexOf('/') + 1);
+ } catch {
+ return "";
+ }
+ }
}
}
@@ -326,6 +484,43 @@ export async function characterURLImport() {
function convertOffSpecCards(charaData:OldTavernChar|CharacterCardV2Risu, imgp:string|undefined = undefined):character{
const data = charaData.spec_version === '2.0' ? charaData.data : charaData
console.log("Off spec detected, converting")
+ const charbook = charaData.spec_version === '2.0' ? charaData.data.character_book : null
+ let lorebook:loreBook[] = []
+ let loresettings:undefined|loreSettings = undefined
+ let loreExt:undefined|any = undefined
+ if(charbook){
+ if((!checkNullish(charbook.recursive_scanning)) &&
+ (!checkNullish(charbook.scan_depth)) &&
+ (!checkNullish(charbook.token_budget))){
+ loresettings = {
+ tokenBudget:charbook.token_budget,
+ scanDepth:charbook.scan_depth,
+ recursiveScanning: charbook.recursive_scanning,
+ fullWordMatching: charbook?.extensions?.risu_fullWordMatching ?? false,
+ }
+ }
+
+ loreExt = charbook.extensions
+
+ for(const book of charbook.entries){
+ lorebook.push({
+ key: book.keys.join(', '),
+ secondkey: book.secondary_keys?.join(', ') ?? '',
+ insertorder: book.insertion_order,
+ comment: book.name ?? book.comment ?? "",
+ content: book.content,
+ mode: "normal",
+ alwaysActive: book.constant ?? false,
+ selective: book.selective ?? false,
+ extentions: {...book.extensions, risu_case_sensitive: book.case_sensitive},
+ activationPercent: book.extensions?.risu_activationPercent,
+ loreCache: book.extensions?.risu_loreCache ?? null,
+ //@ts-ignore
+ useRegex: book.use_regex ?? false
+ })
+ }
+ }
+
return {
name: data.name ?? 'unknown name',
firstMessage: data.first_mes ?? 'unknown first message',
@@ -341,7 +536,7 @@ function convertOffSpecCards(charaData:OldTavernChar|CharacterCardV2Risu, imgp:s
image: imgp,
emotionImages: [],
bias: [],
- globalLore: [],
+ globalLore: lorebook,
viewScreen: 'none',
chaId: uuidv4(),
sdData: defaultSdDataFunc(),
@@ -360,7 +555,10 @@ function convertOffSpecCards(charaData:OldTavernChar|CharacterCardV2Risu, imgp:s
firstMsgIndex: -1,
replaceGlobalNote: "",
triggerscript: [],
- additionalText: ''
+ additionalText: '',
+ loreExt: loreExt,
+ loreSettings: loresettings,
+
}
}
@@ -663,7 +861,7 @@ async function importCharacterCardSpec(card:CharacterCardV2Risu|CharacterCardV3,
alternateGreetings:data.alternate_greetings ?? [],
tags:data.tags ?? [],
creator:data.creator ?? '',
- characterVersion: `${data.character_version}` ?? '',
+ characterVersion: `${data.character_version}` || '',
personality:data.personality ?? '',
scenario:data.scenario ?? '',
firstMsgIndex: -1,
@@ -775,7 +973,7 @@ async function createBaseV2(char:character) {
},
tags: char.tags ?? [],
creator: char.additionalData?.creator ?? '',
- character_version: `${char.additionalData?.character_version}` ?? '',
+ character_version: `${char.additionalData?.character_version}` || '',
extensions: {
risuai: {
// emotions: char.emotionImages,
@@ -850,7 +1048,7 @@ export async function exportCharacterCard(char:character, type:'png'|'json'|'cha
const b64encoded = Buffer.from(await convertImage(rData)).toString('base64')
assetIndex++
card.data.extensions.risuai.emotions[i][1] = `__asset:${assetIndex}`
- await writer.write("chara-ext-asset_" + assetIndex, b64encoded)
+ await writer.write("chara-ext-asset_:" + assetIndex, b64encoded)
}
}
@@ -866,7 +1064,7 @@ export async function exportCharacterCard(char:character, type:'png'|'json'|'cha
const b64encoded = Buffer.from(await convertImage(rData)).toString('base64')
assetIndex++
card.data.extensions.risuai.additionalAssets[i][1] = `__asset:${assetIndex}`
- await writer.write("chara-ext-asset_" + assetIndex, b64encoded)
+ await writer.write("chara-ext-asset_:" + assetIndex, b64encoded)
}
}
@@ -882,7 +1080,7 @@ export async function exportCharacterCard(char:character, type:'png'|'json'|'cha
const b64encoded = Buffer.from(rData).toString('base64')
assetIndex++
card.data.extensions.risuai.vits[key] = `__asset:${assetIndex}`
- await writer.write("chara-ext-asset_" + assetIndex, b64encoded)
+ await writer.write("chara-ext-asset_:" + assetIndex, b64encoded)
}
}
if(type === 'json'){
@@ -923,7 +1121,7 @@ export async function exportCharacterCard(char:character, type:'png'|'json'|'cha
if(type === 'png'){
const b64encoded = Buffer.from(await convertImage(rData)).toString('base64')
card.data.assets[i].uri = `__asset:${assetIndex}`
- await writer.write("chara-ext-asset_" + assetIndex, b64encoded)
+ await writer.write("chara-ext-asset_:" + assetIndex, b64encoded)
}
else if(type === 'json'){
const b64encoded = Buffer.from(await convertImage(rData)).toString('base64')
@@ -1015,6 +1213,20 @@ export async function exportCharacterCard(char:character, type:'png'|'json'|'cha
})
if(type === 'charx'){
+ const md:RisuModule = {
+ name: `${char.name} Module`,
+ description: "Module for " + char.name,
+ id: v4(),
+ trigger: card.data.extensions.risuai.triggerscript ?? [],
+ regex: card.data.extensions.risuai.customScripts ?? [],
+ lorebook: char.globalLore ?? [],
+ }
+ delete card.data.extensions.risuai.triggerscript
+ delete card.data.extensions.risuai.customScripts
+ await writer.write("module.risum", await exportModule(md, {
+ alertEnd: false,
+ saveData: false
+ }))
await writer.write("card.json", Buffer.from(JSON.stringify(card, null, 4)))
}
else{
@@ -1131,7 +1343,7 @@ export function createBaseV3(char:character){
},
tags: char.tags ?? [],
creator: char.additionalData?.creator ?? '',
- character_version: `${char.additionalData?.character_version}` ?? '',
+ character_version: `${char.additionalData?.character_version}` || '',
extensions: {
risuai: {
bias: char.bias,
diff --git a/src/ts/characters.ts b/src/ts/characters.ts
index ac80e335..653a44bc 100644
--- a/src/ts/characters.ts
+++ b/src/ts/characters.ts
@@ -1,17 +1,19 @@
import { get, writable } from "svelte/store";
import { DataBase, saveImage, setDatabase, type character, type Chat, defaultSdDataFunc, type loreBook } from "./storage/database";
-import { alertConfirm, alertError, alertNormal, alertSelect, alertStore, alertWait } from "./alert";
+import { alertAddCharacter, alertConfirm, alertError, alertNormal, alertSelect, alertStore, alertWait } from "./alert";
import { language } from "../lang";
import { decode as decodeMsgpack } from "msgpackr";
import { checkNullish, findCharacterbyId, getUserName, selectMultipleFile, selectSingleFile, sleep } from "./util";
import { v4 as uuidv4 } from 'uuid';
-import { selectedCharID } from "./stores";
+import { MobileGUIStack, OpenRealmStore, selectedCharID } from "./stores";
import { checkCharOrder, downloadFile, getFileSrc } from "./storage/globalApi";
import { reencodeImage } from "./process/files/image";
import { updateInlayScreen } from "./process/inlayScreen";
import { PngChunk } from "./pngChunk";
import { parseMarkdownSafe } from "./parser";
import { translateHTML } from "./translator/translator";
+import { doingChat } from "./process";
+import { importCharacter } from "./characterCards";
export function createNewCharacter() {
let db = get(DataBase)
@@ -247,7 +249,7 @@ export async function exportChat(page:number){
${char.name}
${await htmlChatParse(
- char.firstMsgIndex === -1 ? char.firstMessage : char.alternateGreetings?.[char.firstMsgIndex ?? 0]
+ chat.fmIndex === -1 ? char.firstMessage : char.alternateGreetings?.[chat.fmIndex ?? 0]
)}
${chatContentHTML}
@@ -341,7 +343,8 @@ export async function importChat(){
message: [],
note: "",
name: "Imported Chat",
- localLore: []
+ localLore: [],
+ fmIndex: -1
}
let isFirst = true
@@ -374,6 +377,7 @@ export async function importChat(){
if(json.type === 'risuChat' && json.ver === 1){
const das:Chat = json.data
if(!(checkNullish(das.message) || checkNullish(das.note) || checkNullish(das.name) || checkNullish(das.localLore))){
+ das.fmIndex ??= -1
db.characters[selectedID].chats.unshift(das)
setDatabase(db)
alertNormal(language.successImport)
@@ -412,7 +416,9 @@ function formatTavernChat(chat:string, charName:string){
return chat.replace(/<([Uu]ser)>|\{\{([Uu]ser)\}\}/g, getUserName()).replace(/((\{\{)|<)([Cc]har)(=.+)?((\}\})|>)/g, charName)
}
-export function characterFormatUpdate(index:number|character){
+export function characterFormatUpdate(index:number|character, arg:{
+ updateInteraction?:boolean,
+} = {}){
let db = get(DataBase)
let cha = typeof(index) === 'number' ? db.characters[index] : index
if(cha.chats.length === 0){
@@ -504,10 +510,15 @@ export function characterFormatUpdate(index:number|character){
if(checkNullish(cha.customscript)){
cha.customscript = []
}
+ cha.lastInteraction = Date.now()
if(typeof(index) === 'number'){
db.characters[index] = cha
setDatabase(db)
}
+ cha.chats = cha.chats.map((v) => {
+ v.fmIndex ??= cha.firstMsgIndex ?? -1
+ return v
+ })
return cha
}
@@ -731,4 +742,52 @@ export async function removeChar(index:number,name:string, type:'normal'|'perman
db.characters = chars
setDatabase(db)
selectedCharID.set(-1)
+}
+
+export async function addCharacter(arg:{
+ reseter?:()=>any,
+} = {}){
+ MobileGUIStack.set(100)
+ const reseter = arg.reseter ?? (() => {})
+ const r = await alertAddCharacter()
+ if(r === 'importFromRealm'){
+ selectedCharID.set(-1)
+ OpenRealmStore.set(true)
+ MobileGUIStack.set(0)
+ return
+ }
+ reseter();
+ switch(r){
+ case 'createfromScratch':
+ createNewCharacter()
+ break
+ case 'createGroup':
+ createNewGroup()
+ break
+ case 'importCharacter':
+ await importCharacter()
+ break
+ default:
+ MobileGUIStack.set(1)
+ return
+ }
+ let db = get(DataBase)
+ if(db.characters[db.characters.length-1]){
+ changeChar(db.characters.length-1)
+ }
+ MobileGUIStack.set(1)
+}
+
+export function changeChar(index: number, arg:{
+ reseter?:()=>any,
+} = {}) {
+ const reseter = arg.reseter ?? (() => {})
+ if(get(doingChat)){
+ return
+ }
+ reseter();
+ characterFormatUpdate(index, {
+ updateInteraction: true,
+ });
+ selectedCharID.set(index);
}
\ No newline at end of file
diff --git a/src/ts/drive/backuplocal.ts b/src/ts/drive/backuplocal.ts
index 6e96bba8..5921a6ac 100644
--- a/src/ts/drive/backuplocal.ts
+++ b/src/ts/drive/backuplocal.ts
@@ -1,11 +1,11 @@
-import { BaseDirectory, readBinaryFile, readDir, writeBinaryFile } from "@tauri-apps/api/fs";
+import { BaseDirectory, readFile, readDir, writeFile } from "@tauri-apps/plugin-fs";
import { alertError, alertNormal, alertStore, alertWait } from "../alert";
import { LocalWriter, forageStorage, isTauri } from "../storage/globalApi";
import { decodeRisuSave, encodeRisuSave } from "../storage/risuSave";
import { get } from "svelte/store";
import { DataBase } from "../storage/database";
-import { save } from "@tauri-apps/api/dialog";
-import { relaunch } from "@tauri-apps/api/process";
+import { save } from "@tauri-apps/plugin-dialog";
+import { relaunch } from "@tauri-apps/plugin-process";
import { sleep } from "../util";
import { hubURL } from "../characterCards";
@@ -41,7 +41,7 @@ export async function SaveLocalBackup(){
if(isTauri){
- const assets = await readDir('assets', {dir: BaseDirectory.AppData})
+ const assets = await readDir('assets', {baseDir: BaseDirectory.AppData})
let i = 0;
for(let asset of assets){
i += 1;
@@ -50,7 +50,7 @@ export async function SaveLocalBackup(){
if(!key || !key.endsWith('.png')){
continue
}
- await writer.writeBackup(key, await readBinaryFile(asset.path))
+ await writer.writeBackup(key, await readFile(asset.name, {baseDir: BaseDirectory.AppData}))
}
}
else{
@@ -63,7 +63,7 @@ export async function SaveLocalBackup(){
if(!key || !key.endsWith('.png')){
continue
}
- await writer.writeBackup(key, await forageStorage.getItem(key))
+ await writer.writeBackup(key, await forageStorage.getItem(key) as unknown as Uint8Array)
if(forageStorage.isAccount){
await sleep(1000)
}
@@ -115,7 +115,7 @@ export async function LoadLocalBackup(){
const dbData = await decodeRisuSave(db)
DataBase.set(dbData)
if(isTauri){
- await writeBinaryFile('database/database.bin', db, {dir: BaseDirectory.AppData})
+ await writeFile('database/database.bin', db, {baseDir: BaseDirectory.AppData})
relaunch()
alertStore.set({
type: "wait",
@@ -133,7 +133,7 @@ export async function LoadLocalBackup(){
continue
}
if(isTauri){
- await writeBinaryFile(`assets/` + name, data ,{dir: BaseDirectory.AppData})
+ await writeFile(`assets/` + name, data ,{baseDir: BaseDirectory.AppData})
}
else{
await forageStorage.setItem('assets/' + name, data)
diff --git a/src/ts/drive/drive.ts b/src/ts/drive/drive.ts
index 0aa4ee39..4abfa56a 100644
--- a/src/ts/drive/drive.ts
+++ b/src/ts/drive/drive.ts
@@ -1,10 +1,10 @@
import { get } from "svelte/store";
-import { alertError, alertErrorWait, alertInput, alertNormal, alertSelect, alertStore } from "../alert";
-import { DataBase, setDatabase, type Database } from "../storage/database";
-import { forageStorage, getUnpargeables, isNodeServer, isTauri, openURL } from "../storage/globalApi";
-import { BaseDirectory, exists, readBinaryFile, readDir, writeBinaryFile } from "@tauri-apps/api/fs";
+import { alertError, alertInput, alertNormal, alertSelect, alertStore } from "../alert";
+import { DataBase, type Database } from "../storage/database";
+import { forageStorage, getUnpargeables, isTauri, openURL } from "../storage/globalApi";
+import { BaseDirectory, exists, readFile, readDir, writeFile } from "@tauri-apps/plugin-fs";
import { language } from "../../lang";
-import { relaunch } from '@tauri-apps/api/process';
+import { relaunch } from '@tauri-apps/plugin-process';
import { isEqual } from "lodash";
import { sleep } from "../util";
import { hubURL } from "../characterCards";
@@ -112,89 +112,6 @@ let BackupDb:Database = null
export async function syncDrive() {
BackupDb = structuredClone(get(DataBase))
return
- while(true){
- const maindb = get(DataBase)
- if(maindb?.account?.data?.access_token && maindb?.account?.data?.refresh_token && maindb?.account?.data?.expires_in){
- if(maindb.account.data.expires_in < Date.now()){
- if(!maindb.account){
- alertError("Not logged in error")
- return
- }
- const s = await fetch(hubURL + '/drive/refresh', {
- method: "POST",
- body: JSON.stringify({
- token: maindb.account.token
- })
- })
- if(s.status !== 200){
- alertError(await s.text())
- return
- }
- maindb.account.data = await s.json()
- }
- const ACCESS_TOKEN = maindb.account.data.access_token
- const d = await loadDrive(ACCESS_TOKEN, 'sync')
- lastSaved = Math.floor(Date.now() / 1000)
- localStorage.setItem('risu_lastsaved', `${lastSaved}`)
- const hadNoSync = d === 'noSync'
- if((!isEqual(maindb, BackupDb)) || hadNoSync){
- BackupDb = structuredClone(maindb)
- const files:DriveFile[] = await getFilesInFolder(ACCESS_TOKEN)
- const fileNames = files.map((d) => {
- return d.name
- })
- if(isTauri){
- const assets = await readDir('assets', {dir: BaseDirectory.AppData})
- let i = 0;
- for(let asset of assets){
- i += 1;
- if(hadNoSync){
- alertStore.set({
- type: "wait",
- msg: `Uploading Sync Files... (${i} / ${assets.length})`
- })
- }
- const key = asset.name
- if(!key || !key.endsWith('.png')){
- continue
- }
- const formatedKey = formatKeys(key)
- if(!fileNames.includes(formatedKey)){
- await createFileInFolder(ACCESS_TOKEN, formatedKey, await readBinaryFile(asset.path))
- }
- }
- }
- else{
- const keys = await forageStorage.keys()
-
- for(let i=0;i
();
export const highlighter = (highlightDom:HTMLElement, id:number) => {
- if(highlightDom){
- if(!CSS.highlights){
- return
- }
-
- const walker = document.createTreeWalker(highlightDom, NodeFilter.SHOW_TEXT)
- const nodes:Node[] = []
- let currentNode = walker.nextNode();
- while (currentNode) {
- nodes.push(currentNode);
- currentNode = walker.nextNode();
- }
- const str = "{{char}}"
- if (!str) {
- return;
- }
-
- const ranges:HighlightInt[] = []
-
- nodes.map((el) => {
- const text = el.textContent.toLowerCase()
-
- const cbsParsed = simpleCBSHighlightParser(el,text)
- ranges.push(...cbsParsed)
-
- for(const syntax of highlighterSyntax){
- const regex = syntax.regex
- let match:RegExpExecArray | null;
- while ((match = regex.exec(text)) !== null) {
- const length = match[0].length;
- const index = match.index;
- const range = new Range();
- range.setStart(el, index);
- range.setEnd(el, index + length);
- ranges.push([range, syntax.type])
- }
+ try {
+
+ if(highlightDom){
+ if(!CSS.highlights){
+ return
}
- });
-
- highLights.set(id, ranges)
-
- runHighlight()
+
+ const walker = document.createTreeWalker(highlightDom, NodeFilter.SHOW_TEXT)
+ const nodes:Node[] = []
+ let currentNode = walker.nextNode();
+ while (currentNode) {
+ nodes.push(currentNode);
+ currentNode = walker.nextNode();
+ }
+ const str = "{{char}}"
+ if (!str) {
+ return;
+ }
+
+ const ranges:HighlightInt[] = []
+
+ nodes.map((el) => {
+ const text = el.textContent.toLowerCase()
+
+ const cbsParsed = simpleCBSHighlightParser(el,text)
+ ranges.push(...cbsParsed)
+
+ for(const syntax of highlighterSyntax){
+ const regex = syntax.regex
+ let match:RegExpExecArray | null;
+ while ((match = regex.exec(text)) !== null) {
+ const length = match[0].length;
+ const index = match.index;
+ const range = new Range();
+ range.setStart(el, index);
+ range.setEnd(el, index + length);
+ ranges.push([range, syntax.type])
+ }
+ }
+ });
+
+ highLights.set(id, ranges)
+
+ runHighlight()
+ }
+ } catch (error) {
+
}
}
@@ -80,13 +85,14 @@ export const removeHighlight = (id:number) => {
}
const normalCBS = [
- 'previous_char_chat', 'lastcharmessage', 'previous_user_chat', 'lastusermessage', 'char', 'bot',
- 'user', 'char_persona', 'description', 'char_desc', 'example_dialogue',
+ 'char', 'user', 'char_persona', 'description', 'char_desc', 'example_dialogue', 'previous_char_chat',
+ 'lastcharmessage', 'previous_user_chat', 'lastusermessage',
'example_message', 'persona', 'user_persona', 'lorebook', 'world_info', 'history', 'messages',
'chat_index', 'first_msg_index', 'blank', 'none', 'message_time', 'message_date', 'time',
'date', 'isotime', 'isodate', 'message_idle_duration', 'idle_duration', 'br', 'newline',
'model', 'axmodel', 'role', 'jbtoggled', 'random', 'maxcontext', 'lastmessage', 'lastmessageid',
- 'lastmessageindex', 'emotionlist', 'assetlist', 'prefill_supported', 'unixtime', '/', '/if', '/each', '/pure', '/if_pure', 'slot', 'module_enabled'
+ 'lastmessageindex', 'emotionlist', 'assetlist', 'prefill_supported', 'unixtime', 'slot', 'module_enabled',
+ 'is_first_message', '/', '/if', '/each', '/pure', '/if_pure', '/func', '/pure_display'
]
const normalCBSwithParams = [
@@ -97,15 +103,19 @@ const normalCBSwithParams = [
'arraypop', 'array_pop', 'arraypush', 'array_push', 'arraysplice', 'array_splice',
'makearray', 'array', 'a', 'make_array', 'history', 'messages', 'range', 'date', 'time', 'datetimeformat', 'date_time_format',
'random', 'pick', 'roll', 'datetimeformat', 'hidden_key', 'reverse', 'getglobalvar', 'position', 'slot', 'rollp',
- 'and', 'or', 'not', 'message_time_array', 'filter', 'greater', 'less', 'greater_equal', 'less_equal'
+ 'and', 'or', 'not', 'message_time_array', 'filter', 'greater', 'less', 'greater_equal', 'less_equal', 'arg'
]
const displayRelatedCBS = [
- 'raw', 'img', 'video', 'audio', 'bg', 'emotion', 'asset', 'video-img', 'comment'
+ 'raw', 'img', 'video', 'audio', 'bg', 'emotion', 'asset', 'video-img', 'comment', 'image'
];
+const nestedCBS = [
+ '#if', '#if_pure ', '#pure ', '#each ', '#func', '#pure_display'
+]
+
const specialCBS = [
- '#if', '#if_pure ', '#pure ', '#each ', 'random:', 'pick:', 'roll:', 'datetimeformat:', '? ', 'hidden_key: ', 'reverse: ',
+ 'random:', 'pick:', 'roll:', 'datetimeformat:', '? ', 'hidden_key: ', 'reverse: ', ...nestedCBS
]
const deprecatedCBS = [
@@ -125,6 +135,11 @@ const decorators = [
const deprecatedDecorators = [
'end', 'assistant', 'user', 'system'
]
+
+export const AllCBS = [...normalCBS, ...(normalCBSwithParams.concat(displayRelatedCBS).map((v) => {
+ return v + ':'
+})), ...nestedCBS]
+
const highlighterSyntax = [
{
regex: /<(char|user|bot)>/gi,
diff --git a/src/ts/hotkey.ts b/src/ts/hotkey.ts
index c58b9086..a39f8de3 100644
--- a/src/ts/hotkey.ts
+++ b/src/ts/hotkey.ts
@@ -1,7 +1,9 @@
import { get } from "svelte/store"
-import { alertToast, doingAlert } from "./alert"
+import { alertSelect, alertToast, doingAlert } from "./alert"
import { DataBase, changeToPreset as changeToPreset2 } from "./storage/database"
-import { selectedCharID, settingsOpen } from "./stores"
+import { MobileGUIStack, MobileSideBar, openPersonaList, openPresetList, SafeModeStore, selectedCharID, settingsOpen } from "./stores"
+import { language } from "src/lang"
+import { updateTextThemeAndCSS } from "./gui/colorscheme"
export function initHotkey(){
document.addEventListener('keydown', (ev) => {
@@ -73,6 +75,25 @@ export function initHotkey(){
ev.stopPropagation()
break
}
+ case 'p':{
+ openPresetList.set(!get(openPresetList))
+ ev.preventDefault()
+ ev.stopPropagation()
+ break
+ }
+ case 'e':{
+ openPersonaList.set(!get(openPersonaList))
+ ev.preventDefault()
+ ev.stopPropagation()
+ break
+ }
+ case '.':{
+ SafeModeStore.set(!get(SafeModeStore))
+ updateTextThemeAndCSS()
+ ev.preventDefault()
+ ev.stopPropagation()
+ break
+ }
}
}
if(ev.key === 'Escape'){
@@ -85,6 +106,93 @@ export function initHotkey(){
ev.preventDefault()
}
})
+
+
+ let touchs = 0
+ let touchStartTime = 0
+ //check for triple touch
+ document.addEventListener('touchstart', async (ev) => {
+ touchs++
+ if(touchs > 2){
+ if(Date.now() - touchStartTime > 300){
+ return
+ }
+ touchs = 0
+ if(doingAlert()){
+ return
+ }
+ const selStr = await alertSelect([
+ language.presets,
+ language.persona,
+ language.cancel
+ ])
+ const sel = parseInt(selStr)
+ if(sel === 0){
+ openPresetList.set(!get(openPresetList))
+ }
+ if(sel === 1){
+ openPersonaList.set(!get(openPersonaList))
+ }
+ }
+ if(touchs === 1){
+ touchStartTime = Date.now()
+ }
+ })
+ document.addEventListener('touchend', (ev) => {
+ touchs = 0
+ })
+}
+
+export function initMobileGesture(){
+
+ let pressingPointers = new Map()
+
+ document.addEventListener('touchstart', (ev) => {
+ for(const touch of ev.changedTouches){
+ const ele = touch.target as HTMLElement
+ if(ele.tagName === 'BUTTON' || ele.tagName === 'INPUT' || ele.tagName === 'SELECT' || ele.tagName === 'TEXTAREA'){
+ return
+ }
+ pressingPointers.set(touch.identifier, {x: touch.clientX, y: touch.clientY})
+ }
+ }, {
+ passive: true
+ })
+ document.addEventListener('touchend', (ev) => {
+ for(const touch of ev.changedTouches){
+ const d = pressingPointers.get(touch.identifier)
+ const moveX = touch.clientX - d.x
+ const moveY = touch.clientY - d.y
+ pressingPointers.delete(touch.identifier)
+
+ if(moveX > 50 && Math.abs(moveY) < Math.abs(moveX)){
+ if(get(selectedCharID) === -1){
+ if(get(MobileGUIStack) > 0){
+ MobileGUIStack.update(v => v - 1)
+ }
+ }
+ else{
+ if(get(MobileSideBar) > 0){
+ MobileSideBar.update(v => v - 1)
+ }
+ }
+ }
+ else if(moveX < -50 && Math.abs(moveY) < Math.abs(moveX)){
+ if(get(selectedCharID) === -1){
+ if(get(MobileGUIStack) < 2){
+ MobileGUIStack.update(v => v + 1)
+ }
+ }
+ else{
+ if(get(MobileSideBar) < 3){
+ MobileSideBar.update(v => v + 1)
+ }
+ }
+ }
+ }
+ }, {
+ passive: true
+ })
}
function changeToPreset(num:number){
diff --git a/src/ts/model/names.ts b/src/ts/model/names.ts
index ffc08d98..c0d1f72c 100644
--- a/src/ts/model/names.ts
+++ b/src/ts/model/names.ts
@@ -105,10 +105,18 @@ export function getModelName(name:string){
return 'GPT-4o ChatGPT'
case 'gpt4om':
return 'GPT-4o Mini'
+ case 'gpt4o1-preview':
+ return 'o1 Preview'
+ case 'gpt4o1-mini':
+ return 'o1 Mini'
case 'gpt4om-2024-07-18':
return 'GPT-4o Mini (2024-07-18)'
case 'gemini-1.5-pro-latest':
return 'Gemini 1.5 Pro'
+ case 'gemini-1.5-pro-exp-0801':
+ return 'Gemini 1.5 Pro Exp (0801)'
+ case 'gemini-1.5-pro-exp-0827':
+ return 'Gemini 1.5 Pro Exp (0827)'
case 'gemini-1.5-flash':
return 'Gemini 1.5 Flash'
case 'ollama-hosted':
diff --git a/src/ts/parser.ts b/src/ts/parser.ts
index 3d4ff799..d2af1c79 100644
--- a/src/ts/parser.ts
+++ b/src/ts/parser.ts
@@ -4,17 +4,13 @@ import { DataBase, setDatabase, type Database, type Message, type character, typ
import { getFileSrc } from './storage/globalApi';
import { processScriptFull } from './process/scripts';
import { get } from 'svelte/store';
-import css from '@adobe/css-tools'
-import { CurrentCharacter, CurrentChat, SizeStore, selectedCharID } from './stores';
+import css, { type CssAtRuleAST } from '@adobe/css-tools'
+import { CurrentCharacter, SizeStore, selectedCharID } from './stores';
import { calcString } from './process/infunctions';
import { findCharacterbyId, getPersonaPrompt, getUserIcon, getUserName, parseKeyValue, sfc32, sleep, uuidtoNumber } from './util';
-import { getInlayImage, writeInlayImage } from './process/files/image';
-import { getModuleLorebooks } from './process/modules';
-import { HypaProcesser } from './process/memory/hypamemory';
-import { generateAIImage } from './process/stableDiff';
-import { requestChatData } from './process/request';
+import { getInlayImage } from './process/files/image';
+import { getModuleAssets, getModuleLorebooks } from './process/modules';
import type { OpenAIChat } from './process';
-import { alertInput, alertNormal } from './alert';
import hljs from 'highlight.js/lib/core'
import 'highlight.js/styles/atom-one-dark.min.css'
@@ -79,11 +75,24 @@ DOMPurify.addHook("uponSanitizeAttribute", (node, data) => {
function renderMarkdown(md:markdownit, data:string){
- return md.render(data.replace(/“|”/g, '"').replace(/‘|’/g, "'"))
- .replace(/\uE9b0/gu, '“')
- .replace(/\uE9b1/gu, '” ')
- .replace(/\uE9b2/gu, '‘')
- .replace(/\uE9b3/gu, '’ ')
+ const db = get(DataBase)
+ let quotes = ['“', '”', '‘', '’']
+ if(db?.customQuotes){
+ quotes = db.customQuotesData ?? quotes
+ }
+
+ let text = md.render(data.replace(/“|”/g, '"').replace(/‘|’/g, "'"))
+
+ if(db?.unformatQuotes){
+ text = text.replace(/\uE9b0/gu, quotes[0]).replace(/\uE9b1/gu, quotes[1])
+ text = text.replace(/\uE9b2/gu, quotes[2]).replace(/\uE9b3/gu, quotes[3])
+ }
+ else{
+ text = text.replace(/\uE9b0/gu, '' + quotes[0]).replace(/\uE9b1/gu, quotes[1] + ' ')
+ text = text.replace(/\uE9b2/gu, '' + quotes[2]).replace(/\uE9b3/gu, quotes[3] + ' ')
+ }
+
+ return text
}
async function renderHighlightableMarkdown(data:string) {
@@ -242,7 +251,7 @@ async function renderHighlightableMarkdown(data:string) {
}
-export const assetRegex = /{{(raw|img|video|audio|bg|emotion|asset|video-img|source)::(.+?)}}/g
+export const assetRegex = /{{(raw|path|img|image|video|audio|bg|emotion|asset|video-img|source)::(.+?)}}/g
async function parseAdditionalAssets(data:string, char:simpleCharacterArgument|character, mode:'normal'|'back', mode2:'unset'|'pre'|'post' = 'unset'){
const db = get(DataBase)
@@ -273,6 +282,16 @@ async function parseAdditionalAssets(data:string, char:simpleCharacterArgument|c
}
}
}
+ const moduleAssets = getModuleAssets()
+ if(moduleAssets.length > 0){
+ for(const asset of moduleAssets){
+ const assetPath = await getFileSrc(asset[1])
+ assetPaths[asset[0].toLocaleLowerCase()] = {
+ path: assetPath,
+ ext: asset[2]
+ }
+ }
+ }
const videoExtention = ['mp4', 'webm', 'avi', 'm4p', 'm4v']
let needsSourceAccess = false
data = data.replaceAll(assetRegex, (full:string, type:string, name:string) => {
@@ -301,15 +320,18 @@ async function parseAdditionalAssets(data:string, char:simpleCharacterArgument|c
}
switch(type){
case 'raw':
+ case 'path':
return path.path
case 'img':
return ` `
+ case 'image':
+ return ` \n`
case 'video':
- return ` `
+ return ` \n`
case 'video-img':
- return ` `
+ return ` \n`
case 'audio':
- return ` `
+ return ` \n`
case 'bg':
if(mode === 'back'){
return `
`
@@ -317,9 +339,9 @@ async function parseAdditionalAssets(data:string, char:simpleCharacterArgument|c
break
case 'asset':{
if(path.ext && videoExtention.includes(path.ext)){
- return ` `
+ return ` \n`
}
- return ` `
+ return ` \n`
}
}
return ''
@@ -365,7 +387,13 @@ export interface simpleCharacterArgument{
}
-export async function ParseMarkdown(data:string, charArg:(character|simpleCharacterArgument | groupChat | string) = null, mode:'normal'|'back'|'pretranslate' = 'normal', chatID=-1) {
+export async function ParseMarkdown(
+ data:string,
+ charArg:(character|simpleCharacterArgument | groupChat | string) = null,
+ mode:'normal'|'back'|'pretranslate' = 'normal',
+ chatID=-1,
+ cbsConditions:CbsConditions = {}
+) {
let firstParsed = ''
const additionalAssetMode = (mode === 'back') ? 'back' : 'normal'
let char = (typeof(charArg) === 'string') ? (findCharacterbyId(charArg)) : (charArg)
@@ -374,7 +402,7 @@ export async function ParseMarkdown(data:string, charArg:(character|simpleCharac
firstParsed = data
}
if(char){
- data = (await processScriptFull(char, data, 'editdisplay', chatID)).data
+ data = (await processScriptFull(char, data, 'editdisplay', chatID, cbsConditions)).data
}
if(firstParsed !== data && char && char.type !== 'group'){
data = await parseAdditionalAssets(data, char, additionalAssetMode, 'post')
@@ -421,6 +449,32 @@ function encodeStyle(txt:string){
}
const styleDecodeRegex = /\(.+?)\<\/risu-style\>/gms
+function decodeStyleRule(rule:CssAtRuleAST){
+ if(rule.type === 'rule'){
+ if(rule.selectors){
+ for(let i=0;i {
+ if(v.startsWith('.')){
+ return ".x-risu-" + v.substring(1)
+ }
+ return v
+ }).join(' ')
+
+ rule.selectors[i] = ".chattext " + selectors
+ }
+ }
+ }
+ }
+ if(rule.type === 'media' || rule.type === 'supports' || rule.type === 'document' || rule.type === 'host' || rule.type === 'container' ){
+ for(let i=0;i {
@@ -428,26 +482,10 @@ function decodeStyle(text:string){
const ast = css.parse(Buffer.from(txt, 'hex').toString('utf-8'))
const rules = ast?.stylesheet?.rules
if(rules){
- for(const rule of rules){
-
- if(rule.type === 'rule'){
- if(rule.selectors){
- for(let i=0;i {
- if(v.startsWith('.')){
- return ".x-risu-" + v.substring(1)
- }
- return v
- }).join(' ')
-
- rule.selectors[i] = ".chattext " + selectors
- }
- }
- }
- }
+ for(let i=0;i${css.stringify(ast)}`
@@ -594,6 +632,12 @@ type matcherArg = {
text?:string,
recursiveCount?:number
lowLevelAccess?:boolean
+ cbsConditions:CbsConditions
+}
+
+export type CbsConditions = {
+ firstmsg?:boolean
+ chatRole?:string
}
function basicMatcher (p1:string,matcherArg:matcherArg,vars:{[key:string]:string}|null = null ):{
text:string,
@@ -619,7 +663,7 @@ function basicMatcher (p1:string,matcherArg:matcherArg,vars:{[key:string]:string
}
pointer--
}
- return selchar.firstMsgIndex === -1 ? selchar.firstMessage : selchar.alternateGreetings[selchar.firstMsgIndex]
+ return chat.fmIndex === -1 ? selchar.firstMessage : selchar.alternateGreetings[chat.fmIndex]
}
case 'previous_user_chat':
case 'lastusermessage':{
@@ -633,7 +677,7 @@ function basicMatcher (p1:string,matcherArg:matcherArg,vars:{[key:string]:string
}
pointer--
}
- return selchar.firstMsgIndex === -1 ? selchar.firstMessage : selchar.alternateGreetings[selchar.firstMsgIndex]
+ return chat.fmIndex === -1 ? selchar.firstMessage : selchar.alternateGreetings[chat.fmIndex]
}
return ''
}
@@ -764,7 +808,8 @@ function basicMatcher (p1:string,matcherArg:matcherArg,vars:{[key:string]:string
}
case 'first_msg_index':{
const selchar = db.characters[get(selectedCharID)]
- return selchar.firstMsgIndex.toString()
+ const chat = selchar.chats[selchar.chatPage]
+ return chat.fmIndex.toString()
}
case 'blank':
case 'none':{
@@ -926,11 +971,26 @@ function basicMatcher (p1:string,matcherArg:matcherArg,vars:{[key:string]:string
return db.subModel
}
case 'role': {
+ if(matcherArg.cbsConditions.chatRole){
+ return matcherArg.cbsConditions.chatRole
+ }
+ if(matcherArg.cbsConditions.firstmsg){
+ return 'char'
+ }
if (chatID !== -1) {
const selchar = db.characters[get(selectedCharID)]
return selchar.chats[selchar.chatPage].message[chatID].role;
}
- return matcherArg.role ?? 'role'
+ return matcherArg.role ?? 'null'
+ }
+ case 'isfirstmsg':
+ case 'is_first_msg':
+ case 'is_first_message':
+ case 'isfirstmessage':{
+ if(matcherArg.cbsConditions.firstmsg){
+ return '1'
+ }
+ return '0'
}
case 'jbtoggled':{
return db.jailbreakToggle ? '1' : '0'
@@ -1174,6 +1234,16 @@ function basicMatcher (p1:string,matcherArg:matcherArg,vars:{[key:string]:string
case 'object_element':{
return parseDict(arra[1])[arra[2]] ?? 'null'
}
+ case 'object_assert':
+ case 'dict_assert':
+ case 'dictassert':
+ case 'objectassert':{
+ const dict = parseDict(arra[1])
+ if(!dict[arra[2]]){
+ dict[arra[2]] = arra[3]
+ }
+ return JSON.stringify(dict)
+ }
case 'element':
case 'ele':{
@@ -1218,6 +1288,15 @@ function basicMatcher (p1:string,matcherArg:matcherArg,vars:{[key:string]:string
arr.splice(Number(arra[2]), Number(arra[3]), arra[4])
return makeArray(arr)
}
+ case 'arrayassert':
+ case 'array_assert':{
+ const arr = parseArray(arra[1])
+ const index = Number(arra[2])
+ if(index >= arr.length){
+ arr[index] = arra[3]
+ }
+ return makeArray(arr)
+ }
case 'makearray':
case 'array':
case 'a':
@@ -1232,11 +1311,6 @@ function basicMatcher (p1:string,matcherArg:matcherArg,vars:{[key:string]:string
case 'object':
case 'o':
case 'make_object':{
- //ideas:
- // - {{o::key1:value1::key2:value2}} - its confusing for users
- // - {{o::key1:value1,key2:value2}} - since comma can break the parser, this is not good
- // - {{o::key=value::key2=value2}} - this is good enough I think, just need to escape the equal sign
-
const sliced = arra.slice(1)
let out = {}
@@ -1600,7 +1674,7 @@ const legacyBlockMatcher = (p1:string,matcherArg:matcherArg) => {
return null
}
-type blockMatch = 'ignore'|'parse'|'nothing'|'parse-pure'|'pure'|'each'
+type blockMatch = 'ignore'|'parse'|'nothing'|'parse-pure'|'pure'|'each'|'function'|'pure-display'
function parseArray(p1:string):string[]{
try {
@@ -1643,14 +1717,18 @@ function blockStartMatcher(p1:string,matcherArg:matcherArg):{type:blockMatch,typ
if(p1 === '#pure'){
return {type:'pure'}
}
+ if(p1 === '#pure_display' || p1 === '#puredisplay'){
+ return {type:'pure-display'}
+ }
if(p1.startsWith('#each')){
return {type:'each',type2:p1.substring(5).trim()}
}
if(p1.startsWith('#func')){
const statement = p1.split(' ')
- if(matcherArg.funcName === statement[1]){
- return {type:'parse',funcArg:statement.slice(2)}
+ if(statement.length > 1){
+ return {type:'function',funcArg:statement.slice(1)}
}
+
}
@@ -1666,7 +1744,9 @@ function trimLines(p1:string){
function blockEndMatcher(p1:string,type:{type:blockMatch,type2?:string},matcherArg:matcherArg):string{
switch(type.type){
case 'pure':
- case 'parse-pure':{
+ case 'parse-pure':
+ case 'pure-display':
+ case 'function':{
return p1
}
case 'parse':
@@ -1690,6 +1770,9 @@ export function risuChatParser(da:string, arg:{
visualize?:boolean,
role?:string
runVar?:boolean
+ functions?:Map
+ callStack?:number
+ cbsConditions?:CbsConditions
} = {}):string{
const chatID = arg.chatID ?? -1
const db = arg.db ?? get(DataBase)
@@ -1699,9 +1782,14 @@ export function risuChatParser(da:string, arg:{
if(aChara){
if(typeof(aChara) !== 'string' && aChara.type === 'group'){
- const gc = findCharacterbyId(aChara.chats[aChara.chatPage].message.at(-1).saying ?? '')
- if(gc.name !== 'Unknown Character'){
- chara = gc
+ if(aChara.chats[aChara.chatPage].message.length > 0){
+ const gc = findCharacterbyId(aChara.chats[aChara.chatPage].message.at(-1).saying ?? '')
+ if(gc.name !== 'Unknown Character'){
+ chara = gc
+ }
+ }
+ else{
+ chara = 'bot'
}
}
else{
@@ -1722,12 +1810,20 @@ export function risuChatParser(da:string, arg:{
let stackType = new Uint8Array(512)
let pureModeNest:Map = new Map()
let pureModeNestType:Map = new Map()
- let blockNestType:Map = new Map()
+ let blockNestType:Map = new Map()
let commentMode = false
let commentLatest:string[] = [""]
let commentV = new Uint8Array(512)
let thinkingMode = false
let tempVar:{[key:string]:string} = {}
+ let functions:Map = arg.functions ?? (new Map())
const matcherObj = {
chatID: chatID,
@@ -1740,6 +1836,7 @@ export function risuChatParser(da:string, arg:{
role: arg.role,
runVar: arg.runVar ?? false,
consistantChar: arg.consistantChar ?? false,
+ cbsConditions: arg.cbsConditions ?? {}
}
@@ -1808,7 +1905,10 @@ export function risuChatParser(da:string, arg:{
nested.unshift('')
stackType[nested.length] = 5
blockNestType.set(nested.length, matchResult)
- if(matchResult.type === 'ignore' || matchResult.type === 'pure' || matchResult.type === 'each'){
+ if( matchResult.type === 'ignore' || matchResult.type === 'pure' ||
+ matchResult.type === 'each' || matchResult.type === 'function' ||
+ matchResult.type === 'pure-display'
+ ){
pureModeNest.set(nested.length, true)
pureModeNestType.set(nested.length, "block")
}
@@ -1818,7 +1918,10 @@ export function risuChatParser(da:string, arg:{
if(dat.startsWith('/')){
if(stackType[nested.length] === 5){
const blockType = blockNestType.get(nested.length)
- if(blockType.type === 'ignore' || blockType.type === 'pure' || blockType.type === 'each'){
+ if( blockType.type === 'ignore' || blockType.type === 'pure' ||
+ blockType.type === 'each' || blockType.type === 'function' ||
+ blockType.type === 'pure-display'
+ ){
pureModeNest.delete(nested.length)
pureModeNestType.delete(nested.length)
}
@@ -1837,6 +1940,18 @@ export function risuChatParser(da:string, arg:{
da = da.substring(0, pointer + 1) + added.trim() + da.substring(pointer + 1)
break
}
+ if(blockType.type === 'function'){
+ console.log(matchResult)
+ functions.set(blockType.funcArg[0], {
+ data: matchResult,
+ arg: blockType.funcArg.slice(1)
+ })
+ break
+ }
+ if(blockType.type === 'pure-display'){
+ nested[0] += matchResult.replaceAll('{{', '\\{\\{').replaceAll('}}', '\\}\\}')
+ break
+ }
if(matchResult === ''){
break
}
@@ -1849,6 +1964,26 @@ export function risuChatParser(da:string, arg:{
break
}
}
+ if(dat.startsWith('call::')){
+ if(arg.callStack && arg.callStack > 10){
+ nested[0] += `ERROR: Call stack limit reached`
+ break
+ }
+ const argData = dat.split('::').slice(1)
+ const funcName = argData[0]
+ const func = functions.get(funcName)
+ console.log(func)
+ if(func){
+ let data = func.data
+ for(let i = 0;i < argData.length;i++){
+ data = data.replaceAll(`{{arg::${i}}}`, argData[i])
+ }
+ arg.functions = functions
+ arg.callStack = (arg.callStack ?? 0) + 1
+ nested[0] += risuChatParser(data, arg)
+ break
+ }
+ }
const mc = isPureMode() ? null :basicMatcher(dat, matcherObj, tempVar)
if(!mc && mc !== ''){
nested[0] += `{{${dat}}}`
@@ -1965,424 +2100,6 @@ export function risuChatParser(da:string, arg:{
return nested[0] + result
}
-export async function commandMatcher(p1:string,matcherArg:matcherArg,vars:{[key:string]:string}):Promise<{
- text:string,
- var:{[key:string]:string}
-}|void|null> {
- const arra = p1.split('::')
-
- switch(arra[0]){
- case 'pushchat':
- case 'addchat':
- case 'add_chat':
- case 'push_chat':{
- const chat = get(CurrentChat)
- chat.message.push({role: arra[1] === 'user' ? 'user' : 'char', data: arra[2] ?? ''})
- CurrentChat.set(chat)
- return {
- text: '',
- var: vars
- }
- }
- case 'yield':{
- vars['__return__'] = (vars['__return__'] ?? '') + arra[1]
- return {
- text: '',
- var: vars
- }
- }
- case 'setchat':
- case 'set_chat':{
- const chat = get(CurrentChat)
- const message = chat.message?.at(Number(arra[1]))
- if(message){
- message.data = arra[2] ?? ''
- }
- CurrentChat.set(chat)
- }
- case 'set_chat_role':
- case 'setchatrole':{
- const chat = get(CurrentChat)
- const message = chat.message?.at(Number(arra[1]))
- if(message){
- message.role = arra[2] === 'user' ? 'user' : 'char'
- }
- CurrentChat.set(chat)
- }
- case 'cutchat':
- case 'cut_chat':{
- const chat = get(CurrentChat)
- chat.message = chat.message.slice(Number(arra[1]), Number(arra[2]))
- CurrentChat.set(chat)
- return {
- text: '',
- var: vars
- }
- }
- case 'insertchat':
- case 'insert_chat':{
- const chat = get(CurrentChat)
- chat.message.splice(Number(arra[1]), 0, {role: arra[2] === 'user' ? 'user' : 'char', data: arra[3] ?? ''})
- CurrentChat.set(chat)
- return {
- text: '',
- var: vars
- }
- }
- case 'removechat':
- case 'remove_chat':{
- const chat = get(CurrentChat)
- chat.message.splice(Number(arra[1]), 1)
- CurrentChat.set(chat)
- return {
- text: '',
- var: vars
- }
- }
- case 'regex':{
- const reg = new RegExp(arra[1], arra[2])
- const match = reg.exec(arra[3])
- let res:string[] = []
- for(let i = 0;i < match.length;i++){
- res.push(match[i])
- }
- return {
- text: makeArray(res),
- var: vars
- }
- }
- case 'replace_regex':
- case 'replaceregex':{
- const reg = new RegExp(arra[1], arra[2])
- return {
- text: arra[3].replace(reg, arra[4]),
- var: vars
- }
- }
- case 'call':{
- //WIP
- const called = await risuCommandParser(arra[1], {
- db: matcherArg.db,
- chara: matcherArg.chara,
- funcName: arra[2],
- passed: arra.slice(3),
- recursiveCount: (matcherArg.recursiveCount ?? 0) + 1
- })
-
- return {
- text: called['__return__'] ?? '',
- var: vars
- }
- }
- case 'stop_chat':{
- vars['__stop_chat__'] = '1'
- return {
- text: '',
- var: vars
- }
- }
- case 'similaritysearch':
- case 'similarity_search':
- case 'similarity':{
- if(!matcherArg.lowLevelAccess){
- return {
- text: '',
- var: vars
- }
- }
- const processer = new HypaProcesser('MiniLM')
- const source = arra[1]
- const target = parseArray(arra[2])
- await processer.addText(target)
- const result = await processer.similaritySearch(source)
- return {
- text: makeArray(result),
- var: vars
- }
- }
- case 'image_generation':
- case 'imagegeneration':
- case 'imagegen':
- case 'image_gen':{
- if(!matcherArg.lowLevelAccess){
- return {
- text: '',
- var: vars
- }
- }
- const prompt = arra[1]
- const negative = arra[2] || ''
- const char = matcherArg.chara
-
- const gen = await generateAIImage(prompt, char as character, negative, 'inlay')
- if(!gen){
- return {
- text: 'ERROR: Image generation failed',
- var: vars
- }
- }
- const imgHTML = new Image()
- imgHTML.src = gen
- const inlay = await writeInlayImage(imgHTML)
- return {
- text: inlay,
- var: vars
- }
- }
- case 'send_llm':
- case 'llm':{
- if(!matcherArg.lowLevelAccess){
- return {
- text: '',
- var: vars
- }
- }
- const prompt = parseArray(arra[1])
- let promptbody:OpenAIChat[] = prompt.map((f) => {
- const dict = parseDict(f)
- let role:'system'|'user'|'assistant' = 'assistant'
- switch(dict['role']){
- case 'system':
- case 'sys':
- role = 'system'
- break
- case 'user':
- role = 'user'
- break
- case 'assistant':
- case 'bot':
- case 'char':{
- role = 'assistant'
- break
- }
- }
-
- return {
- content: dict['content'] ?? '',
- role: role,
- }
- })
- const result = await requestChatData({
- formated: promptbody,
- bias: {},
- useStreaming: false,
- noMultiGen: true,
- }, 'model')
-
- if(result.type === 'fail'){
- return {
- text: 'ERROR: ' + result.result,
- var: vars
- }
- }
-
- if(result.type === 'streaming' || result.type === 'multiline'){
- return {
- text: 'ERROR: Streaming and multiline is not supported in this context',
- var: vars
- }
- }
-
- return {
- text: result.result,
- var: vars
- }
- }
- case 'alert':{
- alertNormal(arra[1])
- return {
- text: '',
- var: vars
- }
- }
- case 'input':
- case 'alert_input':
- case 'alertinput':{
- const input = await alertInput(arra[1])
- return {
- text: input,
- var: vars
- }
- }
- }
-
- const matched = basicMatcher(p1, matcherArg)
- if(typeof(matched) === 'string'){
- return {
- text: matched,
- var: vars
- }
- }
-
- return matched
-}
-
-
-export async function risuCommandParser(da:string, arg:{
- db?:Database
- chara?:string|character|groupChat
- funcName?:string
- passed?:string[],
- recursiveCount?:number
- lowLevelAccess?:boolean
-} = {}):Promise<{[key:string]:string}>{
- const db = arg.db ?? get(DataBase)
- const aChara = arg.chara
- let chara:character|string = null
- let passed = arg.passed ?? []
- let recursiveCount = arg.recursiveCount ?? 0
-
- if(recursiveCount > 30){
- return {}
- }
-
- if(aChara){
- if(typeof(aChara) !== 'string' && aChara.type === 'group'){
- const gc = findCharacterbyId(aChara.chats[aChara.chatPage].message.at(-1).saying ?? '')
- if(gc.name !== 'Unknown Character'){
- chara = gc
- }
- }
- else{
- chara = aChara
- }
- }
-
- let pointer = 0;
- let nested:string[] = [""]
- let stackType = new Uint8Array(512)
- let pureModeNest:Map = new Map()
- let pureModeNestType:Map = new Map()
- let blockNestType:Map = new Map()
- const matcherObj = {
- chatID: -1,
- chara: chara,
- rmVar: false,
- db: db,
- var: null,
- tokenizeAccurate: false,
- displaying: false,
- role: null,
- runVar: false,
- consistantChar: false,
- funcName: arg.funcName ?? null,
- text: da,
- recursiveCount: recursiveCount,
- lowLevelAccess: arg.lowLevelAccess ?? false
- }
-
- let tempVar:{[key:string]:string} = {}
-
- const isPureMode = () => {
- return pureModeNest.size > 0
- }
- while(pointer < da.length){
- switch(da[pointer]){
- case '{':{
- if(da[pointer + 1] !== '{' && da[pointer + 1] !== '#'){
- nested[0] += da[pointer]
- break
- }
- pointer++
- nested.unshift('')
- stackType[nested.length] = 1
- break
- }
- case '}':{
- if(da[pointer + 1] !== '}' || nested.length === 1 || stackType[nested.length] !== 1){
- nested[0] += da[pointer]
- break
- }
- pointer++
- const dat = nested.shift()
- if(dat.startsWith('#')){
- if(isPureMode()){
- nested[0] += `{{${dat}}}`
- nested.unshift('')
- stackType[nested.length] = 6
- break
- }
- const matchResult = blockStartMatcher(dat, matcherObj)
- if(matchResult.type === 'nothing'){
- nested[0] += `{{${dat}}}`
- break
- }
- else{
- nested.unshift('')
- stackType[nested.length] = 5
- blockNestType.set(nested.length, matchResult)
- if(matchResult.type === 'ignore' || matchResult.type === 'pure' || matchResult.type === 'each'){
- pureModeNest.set(nested.length, true)
- pureModeNestType.set(nested.length, "block")
- }
- if(matchResult.funcArg){
- for(let i = 0;i < matchResult.funcArg.length;i++){
- tempVar[matchResult.funcArg[i]] = passed[i]
- }
- }
- break
- }
- }
- if(dat.startsWith('/')){
- if(stackType[nested.length] === 5){
- const blockType = blockNestType.get(nested.length)
- if(blockType.type === 'ignore' || blockType.type === 'pure' || blockType.type === 'each'){
- pureModeNest.delete(nested.length)
- pureModeNestType.delete(nested.length)
- }
- blockNestType.delete(nested.length)
- const dat2 = nested.shift()
- const matchResult = blockEndMatcher(dat2.trim(), blockType, matcherObj)
- if(blockType.type === 'each'){
- const subind = blockType.type2.lastIndexOf(' ')
- const sub = blockType.type2.substring(subind + 1)
- const array = parseArray(blockType.type2.substring(0, subind))
- let added = ''
- for(let i = 0;i < array.length;i++){
- const res = matchResult.replaceAll(`{{slot::${sub}}}`, array[i])
- added += res
- }
- da = da.substring(0, pointer + 1) + added.trim() + da.substring(pointer + 1)
- break
- }
- if(matchResult === ''){
- break
- }
- nested[0] += matchResult
- break
- }
- if(stackType[nested.length] === 6){
- const sft = nested.shift()
- nested[0] += sft + `{{${dat}}}`
- break
- }
- }
- const mc = isPureMode() ? {
- text:null,
- var:tempVar
- } : (await commandMatcher(dat, matcherObj, tempVar))
- if(mc && (mc.text || mc.text === '')){
- tempVar = mc.var
- if(tempVar['__force_return__']){
- return tempVar
- }
- nested[0] += mc.text ?? `{{${dat}}}`
- }
- else{
- nested[0] += `{{${dat}}}`
- }
- break
- }
- default:{
- nested[0] += da[pointer]
- break
- }
- }
- pointer++
- }
- return tempVar
-
-}
export function getChatVar(key:string){
diff --git a/src/ts/process/cipherChat.ts b/src/ts/process/cipherChat.ts
deleted file mode 100644
index ad37eaf9..00000000
--- a/src/ts/process/cipherChat.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import type { OpenAIChat } from ".";
-
-
-let lastShift = 0
-
-const cipher:'caesar'|'base64' = 'caesar'
-
-export function cipherChat(chat: OpenAIChat[]): OpenAIChat[] {
-
- const shift = Math.floor(Math.random() * 26) + 1
- lastShift = shift
-
- for(let i = 0; i < chat.length; i++){
- chat[i].content = ciphers[cipher].encode(chat[i].content, shift)
- }
-
- chat.unshift({
- content: ciphers[cipher].prompt.replace("${shift}", shift.toString()),
- role: 'system'
- })
- return chat
-
-}
-
-
-export function decipherChat(chat: string): string {
- return ciphers[cipher].decode(chat, lastShift)
-}
-
-//Caesar Chiper
-const caesarCipher = (text: string, shift: number) => {
- return text
- .split('')
- .map(char => {
- const code = char.charCodeAt(0)
- if ((code >= 65) && (code <= 90)) {
- return String.fromCharCode(((code - 65 + shift) % 26) + 65)
- } else if ((code >= 97) && (code <= 122)) {
- return String.fromCharCode(((code - 97 + shift) % 26) + 97)
- } else {
- return char
- }
- })
- .join('')
-}
-
-const caesarDecipher = (text: string, shift: number) => {
- return text
- .split('')
- .map(char => {
- const code = char.charCodeAt(0)
- if ((code >= 65) && (code <= 90)) {
- const shifted = (code - 65 - shift)
- if(shifted < 0){
- return String.fromCharCode(((code - 65 - shift + 26) % 26) + 65)
- }
- return String.fromCharCode(((code - 65 - shift) % 26) + 65)
- } else if ((code >= 97) && (code <= 122)) {
- const shifted = (code - 97 - shift)
- if(shifted < 0){
- return String.fromCharCode(((code - 97 - shift + 26) % 26) + 97)
- }
- return String.fromCharCode(((code - 97 - shift) % 26) + 97)
- } else {
- return char
- }
- })
- .join('')
-}
-
-const base64Encode = (text: string) => {
- return Buffer.from(text).toString('base64')
-}
-
-const base64Decode = (text: string) => {
- return Buffer.from(text, 'base64').toString('ascii')
-}
-
-
-const ciphers = {
- caesar: {
- encode: caesarCipher,
- decode: caesarDecipher,
- prompt: "You are an expert on The Caesar Cipher. We will communicate in Caesar Cipher. Do not be a translator. We are using shift ${shift}"
- },
- base64: {
- encode: base64Encode,
- decode: base64Decode,
- prompt: "You are an expert on The Base64 Cipher. We will communicate in Base64. Do not be a translator."
- }
-}
\ No newline at end of file
diff --git a/src/ts/process/index.ts b/src/ts/process/index.ts
index 483da9a8..18ad7ac1 100644
--- a/src/ts/process/index.ts
+++ b/src/ts/process/index.ts
@@ -17,10 +17,9 @@ import { groupOrder } from "./group";
import { runTrigger } from "./triggers";
import { HypaProcesser } from "./memory/hypamemory";
import { additionalInformations } from "./embedding/addinfo";
-import { cipherChat, decipherChat } from "./cipherChat";
import { getInlayImage, supportsInlayImage } from "./files/image";
import { getGenerationModelString } from "./models/modelString";
-import { sendPeerChar } from "../sync/multiuser";
+import { connectionOpen, peerRevertChat, peerSafeCheck, peerSync } from "../sync/multiuser";
import { runInlayScreen } from "./inlayScreen";
import { runCharacterJS } from "../plugins/embedscript";
import { addRerolls } from "./prereroll";
@@ -56,8 +55,15 @@ export interface OpenAIChatFull extends OpenAIChat{
export const doingChat = writable(false)
export const chatProcessStage = writable(0)
export const abortChat = writable(false)
+export let previewFormated:OpenAIChat[] = []
-export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:number,signal?:AbortSignal,continue?:boolean,usedContinueTokens?:number} = {}):Promise {
+export async function sendChat(chatProcessIndex = -1,arg:{
+ chatAdditonalTokens?:number,
+ signal?:AbortSignal,
+ continue?:boolean,
+ usedContinueTokens?:number,
+ preview?:boolean
+} = {}):Promise {
chatProcessStage.set(0)
const abortSignal = arg.signal ?? (new AbortController()).signal
@@ -101,9 +107,24 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
}
doingChat.set(true)
+ if(connectionOpen){
+ chatProcessStage.set(4)
+ const peerSafe = await peerSafeCheck()
+ if(!peerSafe){
+ peerRevertChat()
+ doingChat.set(false)
+ alertError(language.otherUserRequesting)
+ return false
+ }
+ await peerSync()
+ chatProcessStage.set(0)
+ }
+
let db = get(DataBase)
+ db.statics.messages += 1
let selectedChar = get(selectedCharID)
const nowChatroom = db.characters[selectedChar]
+ nowChatroom.lastInteraction = Date.now()
let selectedChat = nowChatroom.chatPage
nowChatroom.chats[nowChatroom.chatPage].message = nowChatroom.chats[nowChatroom.chatPage].message.map((v) => {
v.chatId = v.chatId ?? v4()
@@ -591,7 +612,7 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
}
if(nowChatroom.type !== 'group'){
- const firstMsg = nowChatroom.firstMsgIndex === -1 ? nowChatroom.firstMessage : nowChatroom.alternateGreetings[nowChatroom.firstMsgIndex]
+ const firstMsg = currentChat.fmIndex === -1 ? nowChatroom.firstMessage : nowChatroom.alternateGreetings[currentChat.fmIndex]
const chat:OpenAIChat = {
role: 'assistant',
@@ -623,7 +644,9 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
let index = 0
for(const msg of ms){
- let formatedChat = await processScript(nowChatroom,risuChatParser(msg.data, {chara: currentChar, role: msg.role}), 'editprocess')
+ let formatedChat = await processScript(nowChatroom,risuChatParser(msg.data, {chara: currentChar, role: msg.role}), 'editprocess', {
+ chatRole: msg.role,
+ })
let name = ''
if(msg.role === 'char'){
if(msg.saying){
@@ -676,10 +699,25 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
}
let attr:string[] = []
+ let role:'user'|'assistant'|'system' = msg.role === 'user' ? 'user' : 'assistant'
- if(nowChatroom.type === 'group' || (usingPromptTemplate && db.promptSettings.sendName)){
- formatedChat = name + ': ' + formatedChat
- attr.push('nameAdded')
+ if(
+ (nowChatroom.type === 'group' && findCharacterbyIdwithCache(msg.saying).chaId !== currentChar.chaId) ||
+ (nowChatroom.type === 'group' && db.groupOtherBotRole === 'assistant') ||
+ (usingPromptTemplate && db.promptSettings.sendName)
+ ){
+ const form = db.groupTemplate || `<{{char}}\'s Message>\n{{slot}}\n{{char}}\'s Message>`
+ formatedChat = risuChatParser(form, {chara: findCharacterbyIdwithCache(msg.saying).name}).replace('{{slot}}', formatedChat)
+ switch(db.groupOtherBotRole){
+ case 'user':
+ case 'assistant':
+ case 'system':
+ role = db.groupOtherBotRole
+ break
+ default:
+ role = 'assistant'
+ break
+ }
}
if(usingPromptTemplate && db.promptSettings.maxThoughtTagDepth !== -1){
const depth = ms.length - index
@@ -689,7 +727,7 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
}
const chat:OpenAIChat = {
- role: msg.role === 'user' ? 'user' : 'assistant',
+ role: role,
content: formatedChat,
memo: msg.chatId,
attr: attr,
@@ -702,6 +740,7 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
currentTokens += await tokenizer.tokenizeChat(chat)
index++
}
+ console.log(JSON.stringify(chats, null, 2))
const depthPrompts = lorepmt.actives.filter(v => {
return (v.pos === 'depth' && v.depth > 0) || v.pos === 'reverse_depth'
@@ -779,7 +818,7 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
}
let biases:[string,number][] = db.bias.concat(currentChar.bias).map((v) => {
- return [risuChatParser(v[0].replaceAll("\\n","\n"), {chara: currentChar}),v[1]]
+ return [risuChatParser(v[0].replaceAll("\\n","\n").replaceAll("\\r","\r").replaceAll("\\\\","\\"), {chara: currentChar}),v[1]]
})
let memories:OpenAIChat[] = []
@@ -1027,10 +1066,6 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
return v
})
- if(db.cipherChat){
- formated = cipherChat(formated)
- }
-
if(currentChar.depth_prompt && currentChar.depth_prompt.prompt && currentChar.depth_prompt.prompt.length > 0){
//depth_prompt
@@ -1090,6 +1125,11 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
maxContext: maxContextTokens,
}
chatProcessStage.set(3)
+ if(arg.preview){
+ previewFormated = formated
+ return true
+ }
+
const req = await requestChatData({
formated: formated,
biasString: biases,
@@ -1140,9 +1180,6 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
if(!result){
result = ''
}
- if(db.cipherChat){
- result = decipherChat(result)
- }
if(db.removeIncompleteResponse){
result = trimUntilPunctuation(result)
}
@@ -1181,7 +1218,9 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
db.characters[selectedChar].chats[selectedChat] = currentChat
setDatabase(db)
}
- await sayTTS(currentChar, result)
+ if(db.ttsAutoSpeech){
+ await sayTTS(currentChar, result)
+ }
}
else{
const msgs = (req.type === 'success') ? [['char',req.result]] as const
@@ -1191,9 +1230,6 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
for(let i=0;i {
const sliced = messages.slice(messages.length - arg.searchDepth,messages.length)
arg.keys = arg.keys.map(key => key.trim()).filter(key => key.length > 0)
let mText = sliced.map((msg) => {
return msg.data
}).join('||')
+ if(arg.recursiveAdditionalPrompt){
+ mText += arg.recursiveAdditionalPrompt
+ }
if(arg.regex){
for(const regexString of arg.keys){
if(!regexString.startsWith('/')){
@@ -284,11 +288,13 @@ export async function loadLoreBookV3Prompt(){
pos:string,
prompt:string
role:'system'|'user'|'assistant'
- priority:number
+ order:number
tokens:number
+ priority:number
}[] = []
let activatedIndexes:number[] = []
let disabledUIPrompts:string[] = []
+ let matchTimes = 0
while(matching){
matching = false
for(let i=0;i {
+ return b.order - a.order
+ })
+
return {
- actives: activesFiltered.reverse()
+ actives: activesResorted.reverse()
}
}
diff --git a/src/ts/process/lua.ts b/src/ts/process/lua.ts
index 60b7694d..a48d4903 100644
--- a/src/ts/process/lua.ts
+++ b/src/ts/process/lua.ts
@@ -1,8 +1,8 @@
-import { getChatVar, risuChatParser, setChatVar, type simpleCharacterArgument } from "../parser";
+import { getChatVar, hasher, risuChatParser, setChatVar, type simpleCharacterArgument } from "../parser";
import { LuaEngine, LuaFactory } from "wasmoon";
import { DataBase, setDatabase, type Chat, type character, type groupChat } from "../storage/database";
import { get } from "svelte/store";
-import { CurrentCharacter, CurrentChat, CurrentVariablePointer, ReloadGUIPointer, selectedCharID } from "../stores";
+import { CurrentCharacter, CurrentChat, ReloadGUIPointer, selectedCharID } from "../stores";
import { alertError, alertInput, alertNormal } from "../alert";
import { HypaProcesser } from "./memory/hypamemory";
import { generateAIImage } from "./stableDiff";
@@ -104,6 +104,7 @@ export async function runLua(code:string, arg:{
if(!LuaSafeIds.has(id)){
return
}
+ chat = get(CurrentChat)
const message = chat.message?.at(index)
if(message){
message.data = value
@@ -114,6 +115,7 @@ export async function runLua(code:string, arg:{
if(!LuaSafeIds.has(id)){
return
}
+ chat = get(CurrentChat)
const message = chat.message?.at(index)
if(message){
message.role = value === 'user' ? 'user' : 'char'
@@ -124,6 +126,7 @@ export async function runLua(code:string, arg:{
if(!LuaSafeIds.has(id)){
return
}
+ chat = get(CurrentChat)
chat.message = chat.message.slice(start,end)
CurrentChat.set(chat)
})
@@ -131,6 +134,7 @@ export async function runLua(code:string, arg:{
if(!LuaSafeIds.has(id)){
return
}
+ chat = get(CurrentChat)
chat.message.splice(index, 1)
CurrentChat.set(chat)
})
@@ -138,6 +142,7 @@ export async function runLua(code:string, arg:{
if(!LuaSafeIds.has(id)){
return
}
+ chat = get(CurrentChat)
let roleData:'user'|'char' = role === 'user' ? 'user' : 'char'
chat.message.push({role: roleData, data: value})
CurrentChat.set(chat)
@@ -146,6 +151,7 @@ export async function runLua(code:string, arg:{
if(!LuaSafeIds.has(id)){
return
}
+ chat = get(CurrentChat)
let roleData:'user'|'char' = role === 'user' ? 'user' : 'char'
chat.message.splice(index, 0, {role: roleData, data: value})
CurrentChat.set(chat)
@@ -154,6 +160,7 @@ export async function runLua(code:string, arg:{
if(!LuaSafeIds.has(id)){
return
}
+ chat = get(CurrentChat)
chat.message.splice(index, 1)
CurrentChat.set(chat)
})
@@ -161,9 +168,11 @@ export async function runLua(code:string, arg:{
if(!LuaSafeIds.has(id)){
return
}
+ chat = get(CurrentChat)
return chat.message.length
})
luaEngine.global.set('getFullChatMain', (id:string) => {
+ chat = get(CurrentChat)
const data = JSON.stringify(chat.message.map((v) => {
return {
role: v.role,
@@ -178,6 +187,7 @@ export async function runLua(code:string, arg:{
if(!LuaSafeIds.has(id)){
return
}
+ chat = get(CurrentChat)
chat.message = realValue.map((v) => {
return {
role: v.role,
@@ -222,6 +232,10 @@ export async function runLua(code:string, arg:{
return `{{inlay::${inlay}}}`
})
+ luaEngine.global.set('hash', async (id:string, value:string) => {
+ return await hasher(new TextEncoder().encode(value))
+ })
+
luaEngine.global.set('LLMMain', async (id:string, promptStr:string) => {
let prompt:{
role: string,
diff --git a/src/ts/process/models/local.ts b/src/ts/process/models/local.ts
index d68a34db..5342ef2d 100644
--- a/src/ts/process/models/local.ts
+++ b/src/ts/process/models/local.ts
@@ -1,12 +1,11 @@
-import { invoke } from "@tauri-apps/api/tauri";
+import { invoke } from "@tauri-apps/api/core";
import { globalFetch } from "src/ts/storage/globalApi";
import { sleep } from "src/ts/util";
import * as path from "@tauri-apps/api/path";
-import { exists, readTextFile } from "@tauri-apps/api/fs";
+import { exists, readTextFile } from "@tauri-apps/plugin-fs";
import { alertClear, alertError, alertMd, alertWait } from "src/ts/alert";
import { get } from "svelte/store";
import { DataBase } from "src/ts/storage/database";
-import { resolveResource } from '@tauri-apps/api/path'
let serverRunning = false;
export function checkLocalModel():Promise{
diff --git a/src/ts/process/modules.ts b/src/ts/process/modules.ts
index 7965fb37..a7b9c198 100644
--- a/src/ts/process/modules.ts
+++ b/src/ts/process/modules.ts
@@ -1,12 +1,16 @@
import { language } from "src/lang"
-import { alertConfirm, alertError, alertModuleSelect, alertNormal } from "../alert"
+import { alertConfirm, alertError, alertModuleSelect, alertNormal, alertStore } from "../alert"
import { DataBase, setDatabase, type customscript, type loreBook, type triggerscript } from "../storage/database"
-import { downloadFile } from "../storage/globalApi"
+import { AppendableBuffer, downloadFile, isNodeServer, isTauri, readImage, saveAsset } from "../storage/globalApi"
import { get } from "svelte/store"
import { CurrentCharacter, CurrentChat } from "../stores"
-import { selectSingleFile } from "../util"
+import { selectSingleFile, sleep } from "../util"
import { v4 } from "uuid"
import { convertExternalLorebook } from "./lorebook"
+import { encode } from "msgpackr"
+import { decodeRPack, encodeRPack } from "../rpack/rpack_bg"
+import { convertImage } from "../parser"
+import { Capacitor } from "@capacitor/core"
export interface RisuModule{
name: string
@@ -19,25 +23,171 @@ export interface RisuModule{
lowLevelAccess?: boolean
hideIcon?: boolean
backgroundEmbedding?:string
+ assets?:[string,string,string][]
+ namespace?:string
}
-export async function exportModule(module:RisuModule){
- await downloadFile(module.name + '.json', JSON.stringify({
- ...module,
+export async function exportModule(module:RisuModule, arg:{
+ alertEnd?:boolean
+ saveData?:boolean
+} = {}){
+ const alertEnd = arg.alertEnd ?? true
+ const saveData = arg.saveData ?? true
+ const apb = new AppendableBuffer()
+ const writeLength = (len:number) => {
+ const lenbuf = Buffer.alloc(4)
+ lenbuf.writeUInt32LE(len, 0)
+ apb.append(lenbuf)
+ }
+ const writeByte = (byte:number) => {
+ //byte is 0-255
+ const buf = Buffer.alloc(1)
+ buf.writeUInt8(byte, 0)
+ apb.append(buf)
+ }
+
+ const assets = module.assets ?? []
+ module = structuredClone(module)
+ module.assets ??= []
+ module.assets = module.assets.map((asset) => {
+ return [asset[0], '', asset[2]] as [string,string,string]
+ })
+
+ const mainbuf = await encodeRPack(Buffer.from(JSON.stringify({
+ module: module,
type: 'risuModule'
- }, null, 2))
- alertNormal(language.successExport)
+ }, null, 2), 'utf-8'))
+
+ writeByte(111) //magic number
+ writeByte(0) //version
+ writeLength(mainbuf.length)
+ apb.append(mainbuf)
+
+ for(let i=0;i {
+ let pos = 0
+
+ const readLength = () => {
+ const len = buf.readUInt32LE(pos)
+ pos += 4
+ return len
+ }
+ const readByte = () => {
+ const byte = buf.readUInt8(pos)
+ pos += 1
+ return byte
+ }
+ const readData = (len:number) => {
+ const data = buf.subarray(pos, pos + len)
+ pos += len
+ return data
+ }
+
+ if(readByte() !== 111){
+ console.error("Invalid magic number")
+ alertError(language.errors.noData)
+ return
+ }
+ if(readByte() !== 0){ //Version check
+ console.error("Invalid version")
+ alertError(language.errors.noData)
+ return
+ }
+
+ const mainLen = readLength()
+ const mainData = readData(mainLen)
+ const main:{
+ type:'risuModule'
+ module:RisuModule
+ } = JSON.parse(Buffer.from(await decodeRPack(mainData)).toString())
+
+ if(main.type !== 'risuModule'){
+ console.error("Invalid module type")
+ alertError(language.errors.noData)
+ return
+ }
+
+ let module = main.module
+
+ let i = 0
+ while(true){
+ const mark = readByte()
+ if(mark === 0){
+ break
+ }
+ if(mark !== 1){
+ alertError(language.errors.noData)
+ return
+ }
+ const len = readLength()
+ const data = readData(len)
+ module.assets[i][1] = await saveAsset(Buffer.from(await decodeRPack(data)))
+ alertStore.set({
+ type: 'wait',
+ msg: `Loading... (Adding Assets ${i} / ${module.assets.length})`
+ })
+ if(!isTauri && !Capacitor.isNativePlatform() &&!isNodeServer){
+ await sleep(100)
+ }
+ i++
+ }
+ alertStore.set({
+ type: 'none',
+ msg: ''
+ })
+
+ module.id = v4()
+ return module
}
export async function importModule(){
- const f = await selectSingleFile(['json', 'lorebook'])
+ const f = await selectSingleFile(['json', 'lorebook', 'risum'])
if(!f){
return
}
- const file = f.data
+ let fileData = f.data
const db = get(DataBase)
+ if(f.name.endsWith('.risum')){
+ try {
+ const buf = Buffer.from(fileData)
+ const module = await readModule(buf)
+ db.modules.push(module)
+ setDatabase(db)
+ return
+ } catch (error) {
+ console.error(error)
+ alertError(language.errors.noData)
+ }
+ }
try {
- const importData = JSON.parse(Buffer.from(file).toString())
+ const importData = JSON.parse(Buffer.from(fileData).toString())
if(importData.type === 'risuModule'){
if(
(!importData.name)
@@ -109,19 +259,37 @@ function getModuleById(id:string){
return null
}
+function getModuleByIds(ids:string[]){
+ let modules:RisuModule[] = []
+ const db = get(DataBase)
+ for(let i=0;i m.id === ids[i] || (m.namespace === ids[i] && m.namespace))
+ if(module){
+ modules.push(module)
+ }
+ }
+ return modules
+}
+
let lastModules = ''
let lastModuleData:RisuModule[] = []
-export function getModules(ids:string[]){
+export function getModules(){
+ const currentChat = get(CurrentChat)
+ const db = get(DataBase)
+ let ids = db.enabledModules ?? []
+ if (currentChat){
+ ids = ids.concat(currentChat.modules ?? [])
+ }
+ if(db.moduleIntergration){
+ const intList = db.moduleIntergration.split(',').map((s) => s.trim())
+ ids = ids.concat(intList)
+ }
const idsJoined = ids.join('-')
if(lastModules === idsJoined){
return lastModuleData
}
- let modules:RisuModule[] = []
- for(const id of ids){
- const module = getModuleById(id)
- modules.push(module)
- }
+ let modules:RisuModule[] = getModuleByIds(ids)
lastModules = idsJoined
lastModuleData = modules
return modules
@@ -130,12 +298,7 @@ export function getModules(ids:string[]){
export function getModuleLorebooks() {
- const currentChat = get(CurrentChat)
- const db = get(DataBase)
- if (!currentChat) return []
- let moduleIds = currentChat.modules ?? []
- moduleIds = moduleIds.concat(db.enabledModules)
- const modules = getModules(moduleIds)
+ const modules = getModules()
let lorebooks: loreBook[] = []
for (const module of modules) {
if(!module){
@@ -148,14 +311,23 @@ export function getModuleLorebooks() {
return lorebooks
}
+export function getModuleAssets() {
+ const modules = getModules()
+ let assets: [string,string,string][] = []
+ for (const module of modules) {
+ if(!module){
+ continue
+ }
+ if (module.assets) {
+ assets = assets.concat(module.assets)
+ }
+ }
+ return assets
+}
+
export function getModuleTriggers() {
- const currentChat = get(CurrentChat)
- const db = get(DataBase)
- if (!currentChat) return []
- let moduleIds = currentChat.modules ?? []
- moduleIds = moduleIds.concat(db.enabledModules)
- const modules = getModules(moduleIds)
+ const modules = getModules()
let triggers: triggerscript[] = []
for (const module of modules) {
if(!module){
@@ -172,12 +344,7 @@ export function getModuleTriggers() {
}
export function getModuleRegexScripts() {
- const currentChat = get(CurrentChat)
- const db = get(DataBase)
- if (!currentChat) return []
- let moduleIds = currentChat.modules ?? []
- moduleIds = moduleIds.concat(db.enabledModules)
- const modules = getModules(moduleIds)
+ const modules = getModules()
let customscripts: customscript[] = []
for (const module of modules) {
if(!module){
diff --git a/src/ts/process/processzip.ts b/src/ts/process/processzip.ts
index a74d0c5f..a3f85f6d 100644
--- a/src/ts/process/processzip.ts
+++ b/src/ts/process/processzip.ts
@@ -81,6 +81,7 @@ export class CharXReader{
assetPromises:Promise[] = []
excludedFiles:string[] = []
cardData:string|undefined
+ moduleData:Uint8Array|undefined
constructor(){
this.unzip = new fflate.Unzip()
this.unzip.register(fflate.UnzipInflate)
@@ -98,6 +99,9 @@ export class CharXReader{
else if(file.name === 'card.json'){
this.cardData = new TextDecoder().decode(assetData)
}
+ else if(file.name === 'module.risum'){
+ this.moduleData = assetData
+ }
else{
this.assetPromises.push((async () => {
const assetId = await saveAsset(assetData)
diff --git a/src/ts/process/prompt.ts b/src/ts/process/prompt.ts
index 1b5b1aea..84614498 100644
--- a/src/ts/process/prompt.ts
+++ b/src/ts/process/prompt.ts
@@ -1,6 +1,8 @@
import { get } from "svelte/store";
import { tokenizeAccurate } from "../tokenizer";
-import type { Database } from "../storage/database";
+import { DataBase, presetTemplate, setDatabase, type Database } from "../storage/database";
+import { alertError, alertNormal } from "../alert";
+import type { OobaChatCompletionRequestParams } from "../model/ooba";
export type PromptItem = PromptItemPlain|PromptItemTyped|PromptItemChat|PromptItemAuthorNote;
export type PromptType = PromptItem['type'];
@@ -64,3 +66,404 @@ export async function tokenizePreset(prompts:PromptItem[], consti:boolean = fals
}
return total
}
+
+export function detectPromptJSONType(text:string){
+
+ function notNull(x:T|null):x is T{
+ return x !== null && x !== undefined
+ }
+
+ try {
+ const parsed = JSON.parse(text)
+ if(notNull(parsed.chat_completion_source) && Array.isArray(parsed.prompts)&& Array.isArray(parsed.prompt_order)){
+ return "STCHAT"
+ }
+ else if(notNull(parsed.temp) && notNull(parsed.rep_pen) && notNull(parsed.min_length)){
+ return "PARAMETERS"
+ }
+ else if(notNull(parsed.story_string) && notNull(parsed.chat_start)){
+ return "STCONTEXT"
+ }
+ else if(notNull(parsed.input_sequence) && notNull(parsed.output_sequence)){
+ return "STINST"
+ }
+ } catch (e) {}
+ return 'NOTSUPPORTED'
+}
+
+const typePriority = [
+ 'STINST',
+ 'PARAMETERS',
+ 'STCONTEXT',
+ 'STCHAT',
+]
+
+
+type InstData = {
+ "system_prompt": string,
+ "input_sequence": string,
+ "output_sequence": string,
+ "last_output_sequence": string,
+ "system_sequence": string,
+ "stop_sequence": string,
+ "system_sequence_prefix": string,
+ "system_sequence_suffix": string,
+ "first_output_sequence": string,
+ "output_suffix": string,
+ "input_suffix": string,
+ "system_suffix": string,
+ "user_alignment_message": string,
+ "system_same_as_user": boolean,
+ "last_system_sequence": string,
+ "first_input_sequence": string,
+ "last_input_sequence": string,
+ "name": string
+}
+
+export function stChatConvert(pre:any){
+ //ST preset
+ let promptTemplate = []
+
+ function findPrompt(identifier:number){
+ return pre.prompts.find((p:any) => p.identifier === identifier)
+ }
+
+ for(const prompt of pre?.prompt_order?.[0]?.order){
+ if(!prompt?.enabled){
+ continue
+ }
+ const p = findPrompt(prompt?.identifier ?? '')
+ if(p){
+ switch(p.identifier){
+ case 'main':{
+ promptTemplate.push({
+ type: 'plain',
+ type2: 'main',
+ text: p.content ?? "",
+ role: p.role ?? "system"
+ })
+ break
+ }
+ case 'jailbreak':
+ case 'nsfw':{
+ promptTemplate.push({
+ type: 'jailbreak',
+ type2: 'normal',
+ text: p.content ?? "",
+ role: p.role ?? "system"
+ })
+ break
+ }
+ case 'dialogueExamples':
+ case 'charPersonality':
+ case 'scenario':{
+ break //ignore
+ }
+ case 'chatHistory':{
+ promptTemplate.push({
+ type: 'chat',
+ rangeEnd: 'end',
+ rangeStart: 0
+ })
+ break
+ }
+ case 'worldInfoBefore':{
+ promptTemplate.push({
+ type: 'lorebook'
+ })
+ break
+ }
+ case 'worldInfoAfter':{
+ break
+ }
+ case 'charDescription':{
+ promptTemplate.push({
+ type: 'description'
+ })
+ break
+ }
+ case 'personaDescription':{
+ promptTemplate.push({
+ type: 'persona'
+ })
+ break
+ }
+ default:{
+ console.log(p)
+ promptTemplate.push({
+ type: 'plain',
+ type2: 'normal',
+ text: p.content ?? "",
+ role: p.role ?? "system"
+ })
+ }
+ }
+ }
+ else{
+ console.log("Prompt not found", prompt)
+
+ }
+ }
+ if(pre?.assistant_prefill){
+ promptTemplate.push({
+ type: 'postEverything'
+ })
+ promptTemplate.push({
+ type: 'plain',
+ type2: 'main',
+ text: `{{#if {{prefill_supported}}}}${pre?.assistant_prefill}{{/if}}`,
+ role: 'bot'
+ })
+ }
+
+ return promptTemplate
+}
+
+export const OobaParams = [
+ "tokenizer",
+ "min_p",
+ "top_k",
+ "repetition_penalty",
+ "repetition_penalty_range",
+ "typical_p",
+ "tfs",
+ "top_a",
+ "epsilon_cutoff",
+ "eta_cutoff",
+ "guidance_scale",
+ "negative_prompt",
+ "penalty_alpha",
+ "mirostat_mode",
+ "mirostat_tau",
+ "mirostat_eta",
+ "temperature_last",
+ "do_sample",
+ "seed",
+ "encoder_repetition_penalty",
+ "no_repeat_ngram_size",
+ "min_length",
+ "num_beams",
+ "length_penalty",
+ "early_stopping",
+ "truncation_length",
+ "max_tokens_second",
+ "custom_token_bans",
+ "auto_max_new_tokens",
+ "ban_eos_token",
+ "add_bos_token",
+ "skip_special_tokens",
+ "grammar_string"
+]
+
+export function promptConvertion(files:{ name: string, content: string, type:string }[]){
+ let preset = structuredClone(presetTemplate)
+ let instData = {
+ "system_prompt": "",
+ "input_sequence": "",
+ "output_sequence": "",
+ "last_output_sequence": "",
+ "system_sequence": "",
+ "stop_sequence": "",
+ "system_sequence_prefix": "",
+ "system_sequence_suffix": "",
+ "first_output_sequence": "",
+ "output_suffix": "",
+ "input_suffix": "",
+ "system_suffix": "",
+ "user_alignment_message": "",
+ "system_same_as_user": false,
+ "last_system_sequence": "",
+ "first_input_sequence": "",
+ "last_input_sequence": "",
+ "name": ""
+ }
+ let story_string = ''
+ let chat_start = ''
+ preset.name = ''
+
+ let type = ''
+
+ files = files.filter(x=>x.type !== 'NOTSUPPORTED').sort((a,b)=>{
+ return typePriority.indexOf(a.type) - typePriority.indexOf(b.type)
+ })
+
+
+ if(files.findIndex(x=>x.type === 'STINST') !== -1){
+ type = 'STINST'
+ }
+ if(files.findIndex(x=>x.type === 'STCHAT') !== -1){
+ if(type !== ''){
+ alertError(`Both ${type} and STCHAT are not supported together.`)
+ return
+ }
+ type = 'STCHAT'
+ }
+
+ let samplers:string[] = []
+
+ let oobaData:OobaChatCompletionRequestParams = {
+ mode: 'instruct',
+ }
+
+
+ for(let i=0;i {
+ if(getname === ''){
+ getname = setname
+ }
+ let multiplier = arg.multiplier ?? 1
+ if(samplers.includes(getname)){
+ //@ts-ignore
+ preset[setname] = data[getname] * multiplier
+ }
+ else{
+ // @ts-ignore
+ preset[setname] = -1000
+ }
+
+ if(OobaParams.includes(getname)){
+ oobaData[getname] = data[getname]
+ }
+ }
+
+ preset.name ||= instData.name ?? ''
+ switch(file.type){
+ case 'STINST':{
+ instData = data as InstData
+ if(data.system_same_as_user){
+ instData.system_sequence = ''
+ instData.system_sequence_prefix = instData.input_sequence
+ instData.system_sequence_suffix = instData.output_sequence
+ }
+ break
+ }
+ case 'PARAMETERS':{
+ samplers = data.samplers
+ getParam('temperature', 'temp', {multiplier: 100})
+ getParam('top_p')
+ getParam('top_k')
+ getParam('top_a')
+ getParam('min_p')
+ getParam('repetition_penalty', 'rep_pen')
+ getParam('frequencyPenalty', 'freq_pen', {multiplier: 100})
+ getParam('PresensePenalty', 'presence_penalty', {multiplier: 100})
+ for(const key of OobaParams){
+ if(samplers.includes(key) && (data[key] !== undefined) && (data[key] !== null)){
+ oobaData[key] = data[key]
+ }
+ }
+ break
+ }
+ case 'STCONTEXT':{
+ story_string = data.story_string
+ chat_start = data.chat_start
+ break
+ }
+ case 'STCHAT':{
+ samplers = []
+ getParam('temperature', 'temperature', {multiplier: 100})
+ getParam('top_p')
+ getParam('top_k')
+ getParam('top_a')
+ getParam('min_p')
+ getParam('repetition_penalty', 'repetition_penalty')
+ getParam('frequencyPenalty', 'frequency_penalty', {multiplier: 100})
+ getParam('PresensePenalty', 'presence_penalty', {multiplier: 100})
+ const prompts = stChatConvert(data)
+ preset.promptTemplate = prompts
+ }
+ }
+ }
+
+ if(type === 'STCHAT'){
+ preset.aiModel = 'openrouter'
+ preset.subModel = 'openrouter'
+ const db = get(DataBase)
+ db.botPresets.push(preset)
+ setDatabase(db)
+
+ alertNormal('Preset converted successfully. You can find it in bot setting presets')
+ return
+ }
+
+ preset.reverseProxyOobaArgs = oobaData
+
+ preset.promptTemplate = [{
+ type: 'plain',
+ type2: 'main',
+ text: '',
+ role: 'system'
+ },{
+ type: 'description',
+ },{
+ type: 'persona',
+ },{
+ type: 'lorebook',
+ },{
+ type: 'chat',
+ rangeStart: 0,
+ rangeEnd: 'end',
+ }, {
+ type: 'authornote',
+ }, {
+ type: 'plain',
+ type2: 'globalNote',
+ text: '',
+ role: 'system'
+ }]
+
+
+
+ //build a jinja template from the instData
+ let jinja = ''
+
+ jinja += story_string
+ .replace(/{{user}}/gi, '{{risu_user}}')
+ .replace(/{{user}}/gi, '{{risu_user}}')
+ .replace(/{{system_prompt}}/gi, instData.system_prompt)
+ .replace(/{{system}}/gi, instData.system_prompt)
+ .replace(/{{#if (.+?){{\/if}}/gis, '')
+ .replace(/{{(.+?)}}/gi, '')
+ .replace(/\n\n+/g, '\n\n')
+ jinja += chat_start
+ jinja += `{% for message in messages %}`
+ jinja += `{% if message.role == 'user' %}`
+ jinja += instData.input_sequence
+ jinja += `{{ message.content }}`
+ jinja += instData.input_suffix
+ jinja += `{% endif %}`
+ jinja += `{% if message.role == 'assistant' %}`
+ jinja += instData.output_sequence
+ jinja += `{{ message.content }}`
+ jinja += instData.output_suffix
+ jinja += `{% endif %}`
+ jinja += `{% if message.role == 'system' %}`
+ jinja += instData.system_sequence
+ jinja += instData.system_sequence_prefix
+ jinja += `{{ message.content }}`
+ jinja += instData.system_sequence_suffix
+ jinja += instData.system_suffix
+ jinja += `{% endif %}`
+ jinja += `{% endfor %}`
+ jinja += instData.output_sequence
+
+ preset.instructChatTemplate = "jinja"
+ preset.JinjaTemplate = jinja
+ preset.aiModel = 'openrouter'
+ preset.subModel = 'openrouter'
+ preset.useInstructPrompt = true
+
+ preset.name ||= 'Converted from JSON'
+
+
+ const db = get(DataBase)
+ db.botPresets.push(preset)
+ setDatabase(db)
+
+ alertNormal('Preset converted successfully. You can find it in bot setting presets')
+}
\ No newline at end of file
diff --git a/src/ts/process/request.ts b/src/ts/process/request.ts
index e139b6fe..1e052153 100644
--- a/src/ts/process/request.ts
+++ b/src/ts/process/request.ts
@@ -21,6 +21,8 @@ import { runTransformers } from "./transformers";
import {createParser} from 'eventsource-parser'
import {Ollama} from 'ollama/dist/browser.mjs'
import { applyChatTemplate } from "./templates/chatTemplate";
+import { OobaParams } from "./prompt";
+import { extractJSON, getOpenAIJSONSchema } from "./templates/jsonSchema";
@@ -79,6 +81,60 @@ interface OaiFunctions {
};
}
+
+type Parameter = 'temperature'|'top_k'|'repetition_penalty'|'min_p'|'top_a'|'top_p'|'frequency_penalty'|'presence_penalty'
+type ParameterMap = {
+ [key in Parameter]?: string;
+};
+
+function applyParameters(data: { [key: string]: any }, parameters: Parameter[], rename: ParameterMap = {}) {
+ const db = get(DataBase)
+ for(const parameter of parameters){
+ let value = 0
+ switch(parameter){
+ case 'temperature':{
+ value = db.temperature === -1000 ? -1000 : (db.temperature / 100)
+ break
+ }
+ case 'top_k':{
+ value = db.top_k
+ break
+ }
+ case 'repetition_penalty':{
+ value = db.repetition_penalty
+ break
+ }
+ case 'min_p':{
+ value = db.min_p
+ break
+ }
+ case 'top_a':{
+ value = db.top_a
+ break
+ }
+ case 'top_p':{
+ value = db.top_p
+ break
+ }
+ case 'frequency_penalty':{
+ value = db.frequencyPenalty === -1000 ? -1000 : (db.frequencyPenalty / 100)
+ break
+ }
+ case 'presence_penalty':{
+ value = db.PresensePenalty === -1000 ? -1000 : (db.PresensePenalty / 100)
+ break
+ }
+ }
+
+ if(value === -1000){
+ continue
+ }
+
+ data[rename[parameter] ?? parameter] = value
+ }
+ return data
+}
+
export async function requestChatData(arg:requestDataArgument, model:'model'|'submodel', abortSignal:AbortSignal=null):Promise {
const db = get(DataBase)
let trys = 0
@@ -131,7 +187,7 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
let useStreaming = db.useStreaming && arg.useStreaming
arg.continue = arg.continue ?? false
let biasString = arg.biasString ?? []
- const aiModel = (model === 'model' || (!db.advancedBotSettings)) ? db.aiModel : db.subModel
+ const aiModel = model === 'model' ? db.aiModel : db.subModel
const multiGen = (db.genTime > 1 && aiModel.startsWith('gpt') && (!arg.continue)) && (!arg.noMultiGen)
let raiModel = aiModel
@@ -180,6 +236,10 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
case 'gpt4om-2024-07-18':
case 'gpt4o-2024-08-06':
case 'gpt4o-chatgpt':
+ case 'gpt4o1-preview':
+ case 'gpt4o1-mini':
+ case 'jamba-1.5-large':
+ case 'jamba-1.5-medium':
case 'reverse_proxy':{
let formatedChat:OpenAIChatExtra[] = []
for(let i=0;i${formatedChat[i].content}`
+ formatedChat[i].role = 'user'
+ }
+ }
+ }
+
for(let i=0;i 0){
- // @ts-ignore
body.seed = db.generationSeed
}
if(db.putUserOpen){
- // @ts-ignore
body.user = getOpenUserString()
}
- if(aiModel === 'openrouter'){
- if(db.top_k !== 0){
- //@ts-ignore
- body.top_k = db.top_k
+ if(db.jsonSchemaEnabled){
+ body.response_format = {
+ "type": "json_schema",
+ "json_schema": getOpenAIJSONSchema()
}
+ }
+
+ if(aiModel === 'openrouter'){
if(db.openrouterFallback){
- //@ts-ignore
body.route = "fallback"
}
- //@ts-ignore
- body.repetition_penalty = db.repetition_penalty
- //@ts-ignore
- body.min_p = db.min_p
- //@ts-ignore
- body.top_a = db.top_a
- //@ts-ignore
body.transforms = db.openrouterMiddleOut ? ['middle-out'] : []
if(db.openrouterProvider){
- //@ts-ignore
body.provider = {
order: [db.openrouterProvider]
}
}
if(db.useInstructPrompt){
- //@ts-ignore
delete body.messages
-
const prompt = applyChatTemplate(formated)
-
- //@ts-ignore
body.prompt = prompt
}
}
+ body = applyParameters(body,
+ aiModel === 'openrouter' ? ['temperature', 'top_p', 'frequency_penalty', 'presence_penalty', 'repetition_penalty', 'min_p', 'top_a', 'top_k'] : ['temperature', 'top_p', 'frequency_penalty', 'presence_penalty']
+ )
+
if(aiModel === 'reverse_proxy' && db.reverseProxyOobaMode){
const OobaBodyTemplate = db.reverseProxyOobaArgs
@@ -549,6 +615,10 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
if(risuIdentify){
headers["X-Proxy-Risu"] = 'RisuAI'
}
+ if(aiModel.startsWith('jamba')){
+ headers['Authorization'] = 'Bearer ' + db.ai21Key
+ replacerURL = 'https://api.ai21.com/studio/v1/chat/completions'
+ }
if(multiGen){
// @ts-ignore
body.n = db.genTime
@@ -594,11 +664,12 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
url: replacerURL,
})
- let dataUint = new Uint8Array([])
+ let dataUint:Uint8Array|Buffer = new Uint8Array([])
const transtream = new TransformStream( {
async transform(chunk, control) {
dataUint = Buffer.from(new Uint8Array([...dataUint, ...chunk]))
+ let JSONreaded:{[key:string]:string} = {}
try {
const datas = dataUint.toString().split('\n')
let readed:{[key:string]:string} = {}
@@ -607,7 +678,17 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
try {
const rawChunk = data.replace("data: ", "")
if(rawChunk === "[DONE]"){
- control.enqueue(readed)
+ if(db.extractJson && db.jsonSchemaEnabled){
+ for(const key in readed){
+ const extracted = extractJSON(readed[key], db.extractJson)
+ JSONreaded[key] = extracted
+ }
+ console.log(JSONreaded)
+ control.enqueue(JSONreaded)
+ }
+ else{
+ control.enqueue(readed)
+ }
return
}
const choices = JSON.parse(rawChunk).choices
@@ -632,7 +713,17 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
} catch (error) {}
}
}
- control.enqueue(readed)
+ if(db.extractJson && db.jsonSchemaEnabled){
+ for(const key in readed){
+ const extracted = extractJSON(readed[key], db.extractJson)
+ JSONreaded[key] = extracted
+ }
+ console.log(JSONreaded)
+ control.enqueue(JSONreaded)
+ }
+ else{
+ control.enqueue(readed)
+ }
} catch (error) {
}
@@ -699,6 +790,21 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
if(res.ok){
try {
if(multiGen && dat.choices){
+ if(db.extractJson && db.jsonSchemaEnabled){
+
+ const c = dat.choices.map((v:{
+ message:{content:string}
+ }) => {
+ const extracted = extractJSON(v.message.content, db.extractJson)
+ return ["char",extracted]
+ })
+
+ return {
+ type: 'multiline',
+ result: c
+ }
+
+ }
return {
type: 'multiline',
result: dat.choices.map((v) => {
@@ -709,11 +815,33 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
}
if(dat?.choices[0]?.text){
+ if(db.extractJson && db.jsonSchemaEnabled){
+ try {
+ const parsed = JSON.parse(dat.choices[0].text)
+ const extracted = extractJSON(parsed, db.extractJson)
+ return {
+ type: 'success',
+ result: extracted
+ }
+ } catch (error) {
+ console.log(error)
+ return {
+ type: 'success',
+ result: dat.choices[0].text
+ }
+ }
+ }
return {
type: 'success',
result: dat.choices[0].text
}
}
+ if(db.extractJson && db.jsonSchemaEnabled){
+ return {
+ type: 'success',
+ result: extractJSON(dat.choices[0].message.content, db.extractJson)
+ }
+ }
const msg:OpenAIChatFull = (dat.choices[0].message)
return {
type: 'success',
@@ -814,7 +942,7 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
"parameters":payload
}
- const da = await globalFetch("https://api.novelai.net/ai/generate", {
+ const da = await globalFetch(aiModel === 'novelai_kayra' ? "https://text.novelai.net/ai/generate" : "https://api.novelai.net/ai/generate", {
body: body,
headers: {
"Authorization": "Bearer " + db.novelai.token
@@ -889,7 +1017,7 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
case 'mancer':{
let streamUrl = db.textgenWebUIStreamURL.replace(/\/api.*/, "/api/v1/stream")
let blockingUrl = db.textgenWebUIBlockingURL.replace(/\/api.*/, "/api/v1/generate")
- let bodyTemplate:any
+ let bodyTemplate:{[key:string]:any} = {}
const suggesting = model === "submodel"
const prompt = applyChatTemplate(formated)
let stopStrings = getStopStrings(suggesting)
@@ -1035,9 +1163,12 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
const OobaBodyTemplate = db.reverseProxyOobaArgs
const keys = Object.keys(OobaBodyTemplate)
for(const key of keys){
- if(OobaBodyTemplate[key] !== undefined && OobaBodyTemplate[key] !== null){
+ if(OobaBodyTemplate[key] !== undefined && OobaBodyTemplate[key] !== null && OobaParams.includes(key)){
bodyTemplate[key] = OobaBodyTemplate[key]
}
+ else if(bodyTemplate[key]){
+ delete bodyTemplate[key]
+ }
}
const response = await globalFetch(urlStr, {
@@ -1109,7 +1240,7 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
const API_ENDPOINT="us-central1-aiplatform.googleapis.com"
const PROJECT_ID=db.google.projectId
const MODEL_ID= aiModel === 'palm2' ? 'text-bison' :
- 'palm2_unicorn' ? 'text-unicorn' :
+ aiModel ==='palm2_unicorn' ? 'text-unicorn' :
''
const LOCATION_ID="us-central1"
@@ -1153,7 +1284,10 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
case 'gemini-pro':
case 'gemini-pro-vision':
case 'gemini-1.5-pro-latest':
+ case 'gemini-1.5-pro-exp-0801':
+ case 'gemini-1.5-pro-exp-0827':
case 'gemini-1.5-flash':
+ case 'gemini-1.5-pro-002':
case 'gemini-ultra':
case 'gemini-ultra-vision':{
interface GeminiPart{
@@ -1292,11 +1426,11 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
const body = {
contents: reformatedChat,
- generation_config: {
+ generation_config: applyParameters({
"maxOutputTokens": maxTokens,
- "temperature": temperature,
- "topP": db.top_p,
- },
+ }, ['temperature', 'top_p'], {
+ 'top_p': "topP"
+ }),
safetySettings: uncensoredCatagory
}
@@ -1366,14 +1500,20 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
url.pathname = 'api/v1/generate'
}
- const body:KoboldGenerationInputSchema = {
+ const body = applyParameters({
"prompt": prompt,
- "temperature": (db.temperature / 100),
- "top_p": db.top_p,
max_length: maxTokens,
max_context_length: db.maxContext,
n: 1
- }
+ }, [
+ 'temperature',
+ 'top_p',
+ 'repetition_penalty',
+ 'top_k',
+ 'top_a'
+ ], {
+ 'repetition_penalty': 'rep_pen'
+ }) as KoboldGenerationInputSchema
const da = await globalFetch(url.toString(), {
method: "POST",
@@ -1535,7 +1675,11 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
}
}
case 'cohere-command-r':
- case 'cohere-command-r-plus':{
+ case 'cohere-command-r-plus':
+ case 'cohere-command-r-08-2024':
+ case 'cohere-command-r-03-2024':
+ case 'cohere-command-r-plus-04-2024':
+ case 'cohere-command-r-plus-08-2024':{
const modelName = aiModel.replace('cohere-', '')
let lastChatPrompt = ''
let preamble = ''
@@ -1566,10 +1710,7 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
//reformat chat
-
-
-
- let body = {
+ let body = applyParameters({
message: lastChatPrompt,
chat_history: formated.map((v) => {
if(v.role === 'assistant'){
@@ -1594,13 +1735,17 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
}).filter((v) => v !== null).filter((v) => {
return v.message
}),
- temperature: temperature,
- k: db.top_k,
- p: (db.top_p > 0.99) ? 0.99 : (db.top_p < 0.01) ? 0.01 : db.top_p,
- presence_penalty: arg.PresensePenalty || (db.PresensePenalty / 100),
- frequency_penalty: arg.frequencyPenalty || (db.frequencyPenalty / 100),
- }
+ }, [
+ 'temperature', 'top_k', 'top_p', 'presence_penalty', 'frequency_penalty'
+ ], {
+ 'top_k': 'k',
+ 'top_p': 'p',
+ })
+ if(aiModel !== 'cohere-command-r-03-2024' && aiModel !== 'cohere-command-r-plus-04-2024'){
+ body.safety_mode = "NONE"
+ }
+
if(preamble){
if(body.chat_history.length > 0){
// @ts-ignore
@@ -1873,16 +2018,13 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
}
- let body = {
+ let body = applyParameters({
model: raiModel,
messages: finalChat,
system: systemPrompt.trim(),
max_tokens: maxTokens,
- temperature: temperature,
- top_p: db.top_p,
- top_k: db.top_k,
stream: useStreaming ?? false
- }
+ }, ['temperature', 'top_k', 'top_p'])
if(systemPrompt === ''){
delete body.system
@@ -1997,6 +2139,10 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
headers['anthropic-beta'] = 'prompt-caching-2024-07-31'
}
+ if(db.usePlainFetch){
+ headers['anthropic-dangerous-direct-browser-access'] = 'true'
+ }
+
if(useStreaming){
const res = await fetchNative(replacerURL, {
diff --git a/src/ts/process/scripts.ts b/src/ts/process/scripts.ts
index 488329a9..7e90b044 100644
--- a/src/ts/process/scripts.ts
+++ b/src/ts/process/scripts.ts
@@ -5,9 +5,9 @@ import { downloadFile } from "../storage/globalApi";
import { alertError, alertNormal } from "../alert";
import { language } from "src/lang";
import { selectSingleFile } from "../util";
-import { assetRegex, risuChatParser as risuChatParserOrg, type simpleCharacterArgument } from "../parser";
+import { assetRegex, type CbsConditions, risuChatParser as risuChatParserOrg, type simpleCharacterArgument } from "../parser";
import { runCharacterJS } from "../plugins/embedscript";
-import { getModuleRegexScripts } from "./modules";
+import { getModuleAssets, getModuleRegexScripts } from "./modules";
import { HypaProcesser } from "./memory/hypamemory";
import { runLuaEditTrigger } from "./lua";
@@ -16,13 +16,19 @@ const randomness = /\|\|\|/g
export type ScriptMode = 'editinput'|'editoutput'|'editprocess'|'editdisplay'
-export async function processScript(char:character|groupChat, data:string, mode:ScriptMode){
- return (await processScriptFull(char, data, mode)).data
+type pScript = {
+ script: customscript,
+ order: number
+ actions: string[]
}
-export function exportRegex(){
+export async function processScript(char:character|groupChat, data:string, mode:ScriptMode, cbsConditions:CbsConditions = {}){
+ return (await processScriptFull(char, data, mode, -1, cbsConditions)).data
+}
+
+export function exportRegex(s?:customscript[]){
let db = get(DataBase)
- const script = db.globalscript
+ const script = s ?? db.globalscript
const data = Buffer.from(JSON.stringify({
type: 'regex',
data: script
@@ -31,22 +37,22 @@ export function exportRegex(){
alertNormal(language.successExport)
}
-export async function importRegex(){
+export async function importRegex(o?:customscript[]):Promise{
+ o = o ?? []
const filedata = (await selectSingleFile(['json'])).data
if(!filedata){
- return
+ return o
}
let db = get(DataBase)
try {
const imported= JSON.parse(Buffer.from(filedata).toString('utf-8'))
if(imported.type === 'regex' && imported.data){
const datas:customscript[] = imported.data
- const script = db.globalscript
+ const script = o
for(const data of datas){
script.push(data)
}
- db.globalscript = script
- setDatabase(db)
+ return o
}
else{
alertError("File invaid or corrupted")
@@ -55,11 +61,12 @@ export async function importRegex(){
} catch (error) {
alertError(`${error}`)
}
+ return o
}
let bestMatchCache = new Map()
-export async function processScriptFull(char:character|groupChat|simpleCharacterArgument, data:string, mode:ScriptMode, chatID = -1){
+export async function processScriptFull(char:character|groupChat|simpleCharacterArgument, data:string, mode:ScriptMode, chatID = -1, cbsConditions:CbsConditions = {}){
let db = get(DataBase)
let emoChanged = false
const scripts = (db.globalscript ?? []).concat(char.customscript).concat(getModuleRegexScripts())
@@ -72,7 +79,9 @@ export async function processScriptFull(char:character|groupChat|simpleCharacter
if(scripts.length === 0){
return {data, emoChanged}
}
- for (const script of scripts){
+ function executeScript(pscript:pScript){
+ const script = pscript.script
+
if(script.type === mode){
let outScript2 = script.out.replaceAll("$n", "\n")
@@ -81,18 +90,29 @@ export async function processScriptFull(char:character|groupChat|simpleCharacter
if(script.ableFlag){
flag = script.flag || 'g'
}
- if(outScript.startsWith('@@move_top') || outScript.startsWith('@@move_bottom')){
+ if(outScript.startsWith('@@move_top') || outScript.startsWith('@@move_bottom') || pscript.actions.includes('move_top') || pscript.actions.includes('move_bottom')){
flag = flag.replace('g', '') //temperary fix
}
+ if(outScript.endsWith('>') && !pscript.actions.includes('no_end_nl')){
+ outScript += '\n'
+ }
//remove unsupported flag
- flag = flag.replace(/[^gimuy]/g, '')
+ flag = flag.trim().replace(/[^dgimsuvy]/g, '')
+ //remove repeated flags
+ flag = flag.split('').filter((v, i, a) => a.indexOf(v) === i).join('')
+
if(flag.length === 0){
flag = 'u'
}
- const reg = new RegExp(script.in, flag)
- if(outScript.startsWith('@@')){
+ let input = script.in
+ if(pscript.actions.includes('cbs')){
+ input = risuChatParser(input, { chatID: chatID, cbsConditions })
+ }
+
+ const reg = new RegExp(input, flag)
+ if(outScript.startsWith('@@') || pscript.actions.length > 0){
if(reg.test(data)){
if(outScript.startsWith('@@emo ')){
const emoName = script.out.substring(6).trim()
@@ -117,17 +137,19 @@ export async function processScriptFull(char:character|groupChat|simpleCharacter
}
}
}
- if(outScript.startsWith('@@inject') && chatID !== -1){
+ else if((outScript.startsWith('@@inject') || pscript.actions.includes('inject')) && chatID !== -1){
const selchar = db.characters[get(selectedCharID)]
selchar.chats[selchar.chatPage].message[chatID].data = data
data = data.replace(reg, "")
}
- if(outScript.startsWith('@@move_top') || outScript.startsWith('@@move_bottom')){
+ else if(
+ outScript.startsWith('@@move_top') || outScript.startsWith('@@move_bottom') ||
+ pscript.actions.includes('move_top') || pscript.actions.includes('move_bottom')
+ ){
const isGlobal = flag.includes('g')
const matchAll = isGlobal ? data.matchAll(reg) : [data.match(reg)]
data = data.replace(reg, "")
for(const matched of matchAll){
- console.log(matched)
if(matched){
const inData = matched[0]
let out = outScript.replace('@@move_top ', '').replace('@@move_bottom ', '')
@@ -146,8 +168,7 @@ export async function processScriptFull(char:character|groupChat|simpleCharacter
}
return v
})
- console.log(out)
- if(outScript.startsWith('@@move_top')){
+ if(outScript.startsWith('@@move_top') || pscript.actions.includes('move_top')){
data = out + '\n' +data
}
else{
@@ -156,13 +177,16 @@ export async function processScriptFull(char:character|groupChat|simpleCharacter
}
}
}
+ else{
+ data = risuChatParser(data.replace(reg, outScript), { chatID: chatID, cbsConditions })
+ }
}
else{
- if(outScript.startsWith('@@repeat_back') && chatID !== -1){
+ if((outScript.startsWith('@@repeat_back') || pscript.actions.includes('repeat_back')) && chatID !== -1){
const v = outScript.split(' ', 2)[1]
const selchar = db.characters[get(selectedCharID)]
const chat = selchar.chats[selchar.chatPage]
- let lastChat = selchar.firstMsgIndex === -1 ? selchar.firstMessage : selchar.alternateGreetings[selchar.firstMsgIndex]
+ let lastChat = chat.fmIndex === -1 ? selchar.firstMessage : selchar.alternateGreetings[chat.fmIndex]
let pointer = chatID - 1
while(pointer >= 0){
if(chat.message[pointer].role === chat.message[chatID].role){
@@ -197,16 +221,72 @@ export async function processScriptFull(char:character|groupChat|simpleCharacter
}
}
else{
- data = risuChatParser(data.replace(reg, outScript), { chatID: chatID })
+ data = risuChatParser(data.replace(reg, outScript), { chatID: chatID, cbsConditions })
}
}
}
+ let parsedScripts:pScript[] = []
+ let orderChanged = false
+ for (const script of scripts){
+ if(script.ableFlag && script.flag?.includes('<')){
+ const rregex = /<(.+?)>/g
+ const scriptData = structuredClone(script)
+ let order = 0
+ const actions:string[] = []
+ scriptData.flag = scriptData.flag?.replace(rregex, (v:string, p1:string) => {
+ const meta = p1.split(',').map((v) => v.trim())
+ for(const m of meta){
+ if(m.startsWith('order ')){
+ order = parseInt(m.substring(6))
+ }
+ else{
+ actions.push(m)
+ }
+ }
+
+ return ''
+ })
+ parsedScripts.push({
+ script: scriptData,
+ order,
+ actions
+ })
+ continue
+ }
+ parsedScripts.push({
+ script,
+ order: 0,
+ actions: []
+ })
+ }
+
+ if(orderChanged){
+ parsedScripts.sort((a, b) => b.order - a.order) //sort by order
+ }
+ for (const script of parsedScripts){
+ try {
+ executeScript(script)
+ } catch (error) {
+ console.error(error)
+ }
+ }
+
+
+
if(db.dynamicAssets && (char.type === 'simple' || char.type === 'character') && char.additionalAssets && char.additionalAssets.length > 0){
if(!db.dynamicAssetsEditDisplay && mode === 'editdisplay'){
return {data, emoChanged}
}
const assetNames = char.additionalAssets.map((v) => v[0])
+
+ const moduleAssets = getModuleAssets()
+ if(moduleAssets.length > 0){
+ for(const asset of moduleAssets){
+ assetNames.push(asset[0])
+ }
+ }
+
const processer = new HypaProcesser('MiniLM')
await processer.addText(assetNames)
const matches = data.matchAll(assetRegex)
diff --git a/src/ts/process/stableDiff.ts b/src/ts/process/stableDiff.ts
index fea4138c..87e4120c 100644
--- a/src/ts/process/stableDiff.ts
+++ b/src/ts/process/stableDiff.ts
@@ -6,6 +6,7 @@ import { globalFetch, readImage } from "../storage/globalApi"
import { CharEmotion } from "../stores"
import type { OpenAIChat } from "."
import { processZip } from "./processzip"
+import { keiServerURL } from "../kei/kei"
export async function stableDiff(currentChar:character,prompt:string){
let db = get(DataBase)
@@ -488,5 +489,102 @@ export async function generateAIImage(genPrompt:string, currentChar:character, n
return false
}
}
+ if(db.sdProvider === 'kei'){
+ const db = get(DataBase)
+ let auth = db?.account?.token
+ if(!auth){
+ db.account = JSON.parse(localStorage.getItem("fallbackRisuToken"))
+ auth = db?.account?.token
+ db.account.useSync = true
+ }
+ const da = await globalFetch(keiServerURL() + '/imaggen', {
+ body: {
+ "prompt": genPrompt,
+ },
+ headers: {
+ "x-api-key": auth
+ }
+ })
+
+ if(!da.ok || !da.data.success){
+ alertError(Buffer.from(da.data.message || da.data).toString())
+ return false
+ }
+ if(returnSdData === 'inlay'){
+ return da.data.data
+ }
+ else{
+ let charemotions = get(CharEmotion)
+ const img = da.data.data
+ const emos:[string, string,number][] = [[img, img, Date.now()]]
+ charemotions[currentChar.chaId] = emos
+ CharEmotion.set(charemotions)
+ }
+ return returnSdData
+
+ }
+ if(db.sdProvider === 'fal'){
+ const model = db.falModel
+ const token = db.falToken
+
+ let body:{[key:string]:any} = {
+ prompt: genPrompt,
+ enable_safety_checker: false,
+ sync_mode: true,
+ image_size: {
+ "width": db.sdConfig.width,
+ "height": db.sdConfig.height,
+ }
+ }
+
+ if(db.falModel === 'fal-ai/flux-lora'){
+ let loraPath = db.falLora
+ if(loraPath.startsWith('urn:') || loraPath.startsWith('civitai:')){
+ const id = loraPath.split('@').pop()
+ loraPath = `https://civitai.com/api/download/models/${id}?type=Model&format=SafeTensor`
+ }
+ body.loras = [{
+ "path": loraPath,
+ "scale": db.falLoraScale
+ }]
+ }
+
+ if(db.falModel === 'fal-ai/flux-pro'){
+ delete body.enable_safety_checker
+ }
+ console.log(body)
+
+ const res = await globalFetch('https://fal.run/' + model, {
+ headers: {
+ "Authorization": "Key " + token,
+ "Content-Type": "application/json"
+ },
+ method: 'POST',
+ body: body
+ })
+
+ console.log(res)
+
+ if(!res.ok){
+ alertError(JSON.stringify(res.data))
+ return false
+ }
+
+ let image = res.data?.images?.[0]?.url
+ if(!image){
+ alertError(JSON.stringify(res.data))
+ return false
+ }
+
+ if(returnSdData === 'inlay'){
+ return image
+ }
+ else{
+ let charemotions = get(CharEmotion)
+ const emos:[string, string,number][] = [[image, image, Date.now()]]
+ charemotions[currentChar.chaId] = emos
+ CharEmotion.set(charemotions)
+ }
+ }
return ''
}
diff --git a/src/ts/process/templates/chatTemplate.ts b/src/ts/process/templates/chatTemplate.ts
index 71b969b2..f1ff328d 100644
--- a/src/ts/process/templates/chatTemplate.ts
+++ b/src/ts/process/templates/chatTemplate.ts
@@ -11,7 +11,7 @@ export const chatTemplates = {
'chatml': `{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}`,
'gpt2': `{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}`,
'gemma': "{% if messages[0]['role'] == 'system' %}{{ raise_exception('System role not supported') }}{% endif %}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if (message['role'] == 'assistant') %}{% set role = 'model' %}{% else %}{% set role = message['role'] %}{% endif %}{{ '' + role + '\n' + message['content'] | trim + '\n' }}{% endfor %}{% if add_generation_prompt %}{{'model\n'}}{% endif %}",
- 'mistral': "{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ ' [INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + ' ' + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}",
+ 'mistral': "{% for message in messages %}{% if message['role'] == 'user' %}{{ ' [INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + ' ' + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}",
'vicuna': "{%- set ns = namespace(found=false) -%}\n{%- for message in messages -%}\n {%- if message['role'] == 'system' -%}\n {%- set ns.found = true -%}\n {%- endif -%}\n{%- endfor -%}\n{%- if not ns.found -%}\n {{- '' + 'A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user\\'s questions.' + '\\n\\n' -}}\n{%- endif %}\n{%- for message in messages %}\n {%- if message['role'] == 'system' -%}\n {{- '' + message['content'] + '\\n\\n' -}}\n {%- else -%}\n {%- if message['role'] == 'user' -%}\n {{-'USER: ' + message['content'] + '\\n'-}}\n {%- else -%}\n {{-'ASSISTANT: ' + message['content'] + '\\n' -}}\n {%- endif -%}\n {%- endif -%}\n{%- endfor -%}\n{%- if add_generation_prompt -%}\n {{-'ASSISTANT:'-}}\n{%- endif -%}",
"alpaca": "{%- set ns = namespace(found=false) -%}\n{%- for message in messages -%}\n {%- if message['role'] == 'system' -%}\n {%- set ns.found = true -%}\n {%- endif -%}\n{%- endfor -%}\n{%- if not ns.found -%}\n {{- '' + 'Below is an instruction that describes a task. Write a response that appropriately completes the request.' + '\\n\\n' -}}\n{%- endif %}\n{%- for message in messages %}\n {%- if message['role'] == 'system' -%}\n {{- '' + message['content'] + '\\n\\n' -}}\n {%- else -%}\n {%- if message['role'] == 'user' -%}\n {{-'### Instruction:\\n' + message['content'] + '\\n\\n'-}}\n {%- else -%}\n {{-'### Response:\\n' + message['content'] + '\\n\\n' -}}\n {%- endif -%}\n {%- endif -%}\n{%- endfor -%}\n{%- if add_generation_prompt -%}\n {{-'### Response:\\n'-}}\n{%- endif -%}"
}
@@ -22,19 +22,22 @@ export const templateEffect = {
],
'mistral': [
'no_system_messages',
- 'alter_user_assistant_roles'
+ 'alter_user_assistant_roles',
],
} as {[key:string]:TemplateEffect[]}
-export const applyChatTemplate = (messages:OpenAIChat[]) => {
+export const applyChatTemplate = (messages:OpenAIChat[], arg:{
+ type?: string
+ custom?: string
+} = {}) => {
const db = get(DataBase)
const currentChar = get(CurrentCharacter)
- const type = db.instructChatTemplate
+ const type = arg.type ?? db.instructChatTemplate
if(!type){
throw new Error('Template type is not set')
}
let clonedMessages = structuredClone(messages)
- const template = type === 'jinja' ? (new Template(db.JinjaTemplate)) :(new Template(chatTemplates[type]))
+ const template = type === 'jinja' ? (new Template(arg.custom ?? db.JinjaTemplate)) :(new Template(chatTemplates[type]))
let formatedMessages:{
"role": 'user'|'assistant'|'system',
"content": string
@@ -94,6 +97,8 @@ export const applyChatTemplate = (messages:OpenAIChat[]) => {
"messages": formatedMessages,
"add_generation_prompt": true,
"risu_char": currentChar.name,
- "risu_user": getUserName()
+ "risu_user": getUserName(),
+ "eos_token": "",
+ "bos_token": "",
})
}
\ No newline at end of file
diff --git a/src/ts/process/templates/jsonSchema.ts b/src/ts/process/templates/jsonSchema.ts
new file mode 100644
index 00000000..1b162640
--- /dev/null
+++ b/src/ts/process/templates/jsonSchema.ts
@@ -0,0 +1,172 @@
+import { risuChatParser } from "src/ts/parser"
+import { DataBase } from "src/ts/storage/database"
+import { get } from "svelte/store"
+
+export function convertInterfaceToSchema(int:string){
+ if(!int.startsWith('interface ') && !int.startsWith('export interface ')){
+ return JSON.parse(int)
+ }
+
+ int = risuChatParser(int)
+
+ type SchemaProp = {
+ "type": "array"|"string"|"number"|"boolean",
+ "items"?:SchemaProp
+ "enum"?:string[]
+ "const"?:string
+ }
+
+ const lines = int.split('\n')
+ let schema = {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {} as {[key:string]:SchemaProp},
+ "required": [] as string[],
+ }
+ for(let i = 1; i < lines.length; i++){
+ let content = lines[i].trim()
+ if(content === '{'){
+ continue
+ }
+ if(content === '}'){
+ continue
+ }
+ if(content === ''){
+ continue
+ }
+
+ let placeHolders:string[] = []
+
+ content = content
+ .replace(/\\"/gu, '\uE9b4a')
+ .replace(/\\'/gu, '\uE9b4b')
+ .replace(/"(.+?)"/gu, function(match, p1){
+ placeHolders.push(match)
+ return `\uE9b4d${placeHolders.length - 1}`
+ })
+ .replace(/'(.+?)'/gu, function(match, p1){
+ placeHolders.push(`"${p1}"`)
+ return `\uE9b4d${placeHolders.length - 1}`
+ })
+
+ .split('//')[0].trim() //remove comments
+
+ .replace(/((number)|(string)|(boolean))\[\]/gu, 'Array<$1>')
+
+ if(content.endsWith(',') || content.endsWith(';')){
+ content = content.slice(0, -1)
+ }
+
+ let spData = content.replace(/ /g, '').split(':')
+
+ if(spData.length !== 2){
+ throw "SyntaxError Found"
+ }
+
+ let [property,typeData] = spData
+
+ switch(typeData){
+ case 'string':
+ case 'number':
+ case 'boolean':{
+ schema.properties[property] = {
+ type: typeData
+ }
+ break
+ }
+ case 'Array':
+ case 'Array':
+ case 'Array':{
+ const ogType = typeData.slice(6,-1)
+
+ schema.properties[property] = {
+ type: 'array',
+ items: {
+ type: ogType as 'string'|'number'|'boolean'
+ }
+ }
+ break
+ }
+ default:{
+ const types = typeData.split("|")
+ const strings:string[] = []
+ for(const t of types){
+ if(!t.startsWith('\uE9b4d')){
+ throw "Unsupported Type Detected"
+ }
+ const textIndex = t.replace('\uE9b4d','')
+ const text = placeHolders[parseInt(textIndex)]
+ const textParsed = JSON.parse(text.replace(/\uE9b4a/gu, '\\"').replace(/\uE9b4b/gu, "\\'"))
+ strings.push(textParsed)
+ }
+ if(strings.length === 1){
+ schema.properties[property] = {
+ type: 'string',
+ const: strings[0]
+ }
+ }
+ else{
+ schema.properties[property] = {
+ type: 'string',
+ enum: strings
+ }
+ }
+ }
+ }
+
+ schema.required.push(property)
+
+ }
+ return schema
+}
+
+export function getOpenAIJSONSchema(){
+ const db = get(DataBase)
+ const schema = {
+ "name": "format",
+ "strict": db.strictJsonSchema,
+ "schema": convertInterfaceToSchema(db.jsonSchema)
+ }
+ return schema
+}
+
+export function extractJSON(data:string, format:string){
+ const extract = (data:any, format:string) => {
+ try {
+ if(data === undefined || data === null){
+ return ''
+ }
+
+ const fp = format.split('.')
+ const current = data[fp[0]]
+
+ if(current === undefined){
+ return ''
+ }
+ else if(fp.length === 1){
+ return `${current ?? ''}`
+ }
+ else if(typeof current === 'object'){
+ return extractJSON(current, fp.slice(1).join('.'))
+ }
+ else if(Array.isArray(current)){
+ const index = parseInt(fp[1])
+ return extractJSON(current[index], fp.slice(1).join('.'))
+ }
+ else{
+ return `${current ?? ''}`
+ }
+ } catch (error) {
+ return ''
+ }
+ }
+ try {
+ format = risuChatParser(format)
+ data = data.trim()
+ if(data.startsWith('{')){
+ return extract(JSON.parse(data), format)
+ }
+ } catch (error) {}
+ return data
+}
\ No newline at end of file
diff --git a/src/ts/process/triggers.ts b/src/ts/process/triggers.ts
index 247b6470..3c47a247 100644
--- a/src/ts/process/triggers.ts
+++ b/src/ts/process/triggers.ts
@@ -1,9 +1,9 @@
-import { parseChatML, risuChatParser, risuCommandParser } from "../parser";
+import { parseChatML, risuChatParser } from "../parser";
import { DataBase, type Chat, type character } from "../storage/database";
import { tokenize } from "../tokenizer";
import { getModuleTriggers } from "./modules";
import { get } from "svelte/store";
-import { CurrentCharacter, CurrentChat, selectedCharID } from "../stores";
+import { CurrentCharacter, CurrentChat, ReloadGUIPointer, selectedCharID } from "../stores";
import { processMultiCommand } from "./command";
import { parseKeyValue } from "../util";
import { alertError, alertInput, alertNormal, alertSelect } from "../alert";
@@ -301,28 +301,34 @@ export async function runTrigger(char:character,mode:triggerMode, arg:{
case'setvar': {
const effectValue = risuChatParser(effect.value,{chara:char})
const varKey = risuChatParser(effect.var,{chara:char})
+ let originalVar = Number(getVar(varKey))
+ if(Number.isNaN(originalVar)){
+ originalVar = 0
+ }
+ let resultValue = ''
switch(effect.operator){
case '=':{
- setVar(varKey, effectValue)
+ resultValue = effectValue
break
}
case '+=':{
- setVar(varKey, (Number(getVar(varKey)) + Number(effectValue)).toString())
+ resultValue = (originalVar + Number(effectValue)).toString()
break
}
case '-=':{
- setVar(varKey, (Number(getVar(varKey)) - Number(effectValue)).toString())
+ resultValue = (originalVar - Number(effectValue)).toString()
break
}
case '*=':{
- setVar(varKey, (Number(getVar(varKey)) * Number(effectValue)).toString())
+ resultValue = (originalVar * Number(effectValue)).toString()
break
}
case '/=':{
- setVar(varKey, (Number(getVar(varKey)) / Number(effectValue)).toString())
+ resultValue = (originalVar / Number(effectValue)).toString()
break
}
}
+ setVar(varKey, resultValue)
break
}
case 'systemprompt':{
@@ -498,19 +504,6 @@ export async function runTrigger(char:character,mode:triggerMode, arg:{
setVar(effect.inputVar, res)
break
}
-
- case 'triggercode':{
- const triggerCodeResult = await risuCommandParser(effect.code,{
- chara:char,
- lowLevelAccess: trigger.lowLevelAccess,
- funcName: mode
- })
-
- if(triggerCodeResult['__stop_chat__'] === '1'){
- stopSending = true
- }
- break
- }
case 'triggerlua':{
const triggerCodeResult = await runLua(effect.code,{
lowLevelAccess: trigger.lowLevelAccess,
@@ -524,7 +517,7 @@ export async function runTrigger(char:character,mode:triggerMode, arg:{
if(triggerCodeResult.stopSending){
stopSending = true
}
- chat = triggerCodeResult.chat
+ chat = get(CurrentChat)
break
}
}
@@ -544,6 +537,7 @@ export async function runTrigger(char:character,mode:triggerMode, arg:{
if(varChanged){
const currentChat = get(CurrentChat)
currentChat.scriptstate = chat.scriptstate
+ ReloadGUIPointer.set(get(ReloadGUIPointer) + 1)
}
return {additonalSysPrompt, chat, tokens:caculatedTokens, stopSending, sendAIprompt}
diff --git a/src/ts/process/tts.ts b/src/ts/process/tts.ts
index db3e32c2..18b39caf 100644
--- a/src/ts/process/tts.ts
+++ b/src/ts/process/tts.ts
@@ -2,7 +2,7 @@ import { get } from "svelte/store";
import { alertError } from "../alert";
import { DataBase, type character } from "../storage/database";
import { runTranslator, translateVox } from "../translator/translator";
-import { globalFetch } from "../storage/globalApi";
+import { globalFetch, loadAsset } from "../storage/globalApi";
import { language } from "src/lang";
import { getCurrentCharacter, sleep } from "../util";
import { registerOnnxModel, runVITS } from "./transformers";
@@ -27,7 +27,7 @@ export async function sayTTS(character:character,text:string) {
text = text.replace(/\*/g,'')
if(character.ttsReadOnlyQuoted){
- const matches = text.match(/"(.*?)"/g)
+ const matches = text.match(/["「](.*?)["」]/g)
if(matches && matches.length > 0){
text = matches.map(match => match.slice(1, -1)).join("");
}
@@ -231,12 +231,94 @@ export async function sayTTS(character:character,text:string) {
case 'vits':{
await runVITS(text, character.vits)
}
+ case 'gptsovits':{
+ const audioContext = new AudioContext();
+
+ const audio: Uint8Array = await loadAsset(character.gptSoVitsConfig.ref_audio_data.assetId);
+ const base64Audio = btoa(new Uint8Array(audio).reduce((data, byte) => data + String.fromCharCode(byte), ''));
+
+ const body = {
+ text: text,
+ text_lang: character.gptSoVitsConfig.text_lang,
+ ref_audio_path: undefined,
+ ref_audio_name: character.gptSoVitsConfig.ref_audio_data.fileName,
+ ref_audio_data: base64Audio,
+ prompt_text: undefined,
+ prompt_lang: character.gptSoVitsConfig.prompt_lang,
+ top_p: character.gptSoVitsConfig.top_p,
+ temperature: character.gptSoVitsConfig.temperature,
+ speed_factor: character.gptSoVitsConfig.speed,
+ top_k: character.gptSoVitsConfig.top_k,
+ text_split_method: character.gptSoVitsConfig.text_split_method,
+ parallel_infer: true,
+ // media_type: character.gptSoVitsConfig.ref_audio_data.fileName.split('.')[1],
+ ref_free: character.gptSoVitsConfig.use_long_audio || !character.gptSoVitsConfig.use_prompt,
+ }
+
+ if (character.gptSoVitsConfig.use_prompt){
+ body.prompt_text = character.gptSoVitsConfig.prompt
+ }
+
+ if (character.gptSoVitsConfig.use_auto_path){
+ console.log('auto')
+ const path = await globalFetch(`${character.gptSoVitsConfig.url}/get_path`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ rawResponse: false,
+ plainFetchDeforce: true,
+ })
+ console.log(path)
+ if(path.ok){
+ body.ref_audio_path = path.data.message + '/public/audio/' + character.gptSoVitsConfig.ref_audio_data.fileName
+ }
+ else{
+ throw new Error('Failed to Auto get path')
+ }
+ } else {
+ body.ref_audio_path = character.gptSoVitsConfig.ref_audio_path + '/public/audio/' + character.gptSoVitsConfig.ref_audio_data.fileName
+ }
+ console.log(body)
+
+ const response = await globalFetch(`${character.gptSoVitsConfig.url}/tts`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: body,
+ rawResponse: true,
+ })
+ console.log(response)
+
+ if (response.ok) {
+ const audioBuffer = response.data.buffer;
+ audioContext.decodeAudioData(audioBuffer, (decodedData) => {
+ const sourceNode = audioContext.createBufferSource();
+ sourceNode.buffer = decodedData;
+
+ const gainNode = audioContext.createGain();
+ gainNode.gain.value = character.gptSoVitsConfig.volume || 1.0;
+
+ sourceNode.connect(gainNode);
+ gainNode.connect(audioContext.destination);
+
+ sourceNode.start();
+ });
+ } else {
+ const textBuffer: Uint8Array = response.data.buffer
+ const text = Buffer.from(textBuffer).toString('utf-8')
+ throw new Error(text);
+ }
+ }
}
} catch (error) {
alertError(`TTS Error: ${error}`)
}
}
+
+
export const oaiVoices = [
'alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'
]
diff --git a/src/ts/rpack/LICENSE b/src/ts/rpack/LICENSE
new file mode 100644
index 00000000..a3692426
--- /dev/null
+++ b/src/ts/rpack/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C) 2024 Kwaroran
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C) 2024 Kwaroran
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+ .
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
\ No newline at end of file
diff --git a/src/ts/rpack/rpack_bg.js b/src/ts/rpack/rpack_bg.js
new file mode 100644
index 00000000..cf1413f9
--- /dev/null
+++ b/src/ts/rpack/rpack_bg.js
@@ -0,0 +1,85 @@
+import init from './rpack_bg.wasm?init';
+let wasm
+
+let cachedUint8ArrayMemory0 = null;
+
+function getUint8ArrayMemory0() {
+ if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
+ cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
+ }
+ return cachedUint8ArrayMemory0;
+}
+
+async function initWasm() {
+ if(wasm) {
+ return null
+ }
+ const instance = await init()
+ wasm = instance.exports;
+ return;
+}
+
+let WASM_VECTOR_LEN = 0;
+
+function passArray8ToWasm0(arg, malloc) {
+ const ptr = malloc(arg.length * 1, 1) >>> 0;
+ getUint8ArrayMemory0().set(arg, ptr / 1);
+ WASM_VECTOR_LEN = arg.length;
+ return ptr;
+}
+
+let cachedDataViewMemory0 = null;
+
+function getDataViewMemory0() {
+ if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
+ cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
+ }
+ return cachedDataViewMemory0;
+}
+
+function getArrayU8FromWasm0(ptr, len) {
+ ptr = ptr >>> 0;
+ return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
+}
+/**
+* @param {Uint8Array} datas
+* @returns {Promise}
+*/
+export async function encodeRPack(datas) {
+ await initWasm();
+ try {
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
+ const ptr0 = passArray8ToWasm0(datas, wasm.__wbindgen_malloc);
+ const len0 = WASM_VECTOR_LEN;
+ wasm.encode(retptr, ptr0, len0);
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
+ var v2 = getArrayU8FromWasm0(r0, r1).slice();
+ wasm.__wbindgen_free(r0, r1 * 1, 1);
+ return v2;
+ } finally {
+ wasm.__wbindgen_add_to_stack_pointer(16);
+ }
+}
+
+/**
+* @param {Uint8Array} datas
+* @returns {Promise}
+*/
+export async function decodeRPack(datas) {
+ await initWasm();
+ try {
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
+ const ptr0 = passArray8ToWasm0(datas, wasm.__wbindgen_malloc);
+ const len0 = WASM_VECTOR_LEN;
+ wasm.decode(retptr, ptr0, len0);
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
+ var v2 = getArrayU8FromWasm0(r0, r1).slice();
+ wasm.__wbindgen_free(r0, r1 * 1, 1);
+ return v2;
+ } finally {
+ wasm.__wbindgen_add_to_stack_pointer(16);
+ }
+}
+
diff --git a/src/ts/rpack/rpack_bg.wasm b/src/ts/rpack/rpack_bg.wasm
new file mode 100644
index 00000000..877e84f4
Binary files /dev/null and b/src/ts/rpack/rpack_bg.wasm differ
diff --git a/src/ts/storage/accountStorage.ts b/src/ts/storage/accountStorage.ts
index 41d1fd00..5ac66973 100644
--- a/src/ts/storage/accountStorage.ts
+++ b/src/ts/storage/accountStorage.ts
@@ -2,12 +2,14 @@ import { get, writable } from "svelte/store"
import { DataBase } from "./database"
import { hubURL } from "../characterCards"
import localforage from "localforage"
-import { alertLogin, alertStore } from "../alert"
+import { alertError, alertLogin, alertStore, alertWait } from "../alert"
import { forageStorage, getUnpargeables, replaceDbResources } from "./globalApi"
import { encodeRisuSave } from "./risuSave"
import { v4 } from "uuid"
+import { language } from "src/lang"
export const AccountWarning = writable('')
+const risuSession = Date.now().toFixed(0)
let seenWarnings:string[] = []
@@ -26,7 +28,8 @@ export class AccountStorage{
'content-type': 'application/json',
'x-risu-key': key,
'x-risu-auth': this.auth,
- 'X-Format': 'nocheck'
+ 'X-Format': 'nocheck',
+ 'x-risu-session': risuSession
}
})
if(da.headers.get('Content-Type') === 'application/json'){
@@ -37,6 +40,11 @@ export class AccountStorage{
AccountWarning.set(json.warning)
}
}
+ if(json?.reloadSession){
+ alertWait(language.reloadSession)
+ location.reload()
+ return
+ }
}
if(da.status === 304){
diff --git a/src/ts/storage/autoStorage.ts b/src/ts/storage/autoStorage.ts
index 1157b171..2c599b4d 100644
--- a/src/ts/storage/autoStorage.ts
+++ b/src/ts/storage/autoStorage.ts
@@ -2,7 +2,7 @@ import localforage from "localforage"
import { isNodeServer, replaceDbResources } from "./globalApi"
import { NodeStorage } from "./nodeStorage"
import { OpfsStorage } from "./opfsStorage"
-import { alertSelect, alertStore } from "../alert"
+import { alertInput, alertSelect, alertStore } from "../alert"
import { get } from "svelte/store"
import { DataBase, type Database } from "./database"
import { AccountStorage } from "./accountStorage"
@@ -67,6 +67,13 @@ export class AutoStorage{
return true
}
}
+
+ const confirm = await alertInput(`to overwrite your data, type "RISUAI"`)
+ if(confirm !== "RISUAI"){
+ localStorage.setItem('dosync', 'avoid')
+ return false
+ }
+
let replaced:{[key:string]:string} = {}
for(const key of keys){
diff --git a/src/ts/storage/database.ts b/src/ts/storage/database.ts
index f0c80c32..35bbdd6f 100644
--- a/src/ts/storage/database.ts
+++ b/src/ts/storage/database.ts
@@ -1,6 +1,6 @@
export const DataBase = writable({} as any as Database)
export const loadedStore = writable(false)
-export let appVer = "124.2.2"
+export let appVer = "136.0.1"
export let webAppSubVer = ''
import { get, writable } from 'svelte/store';
@@ -428,7 +428,21 @@ export function setDatabase(data:Database){
negInputName: 'text',
timeout: 30
}
-
+ data.hideApiKey ??= true
+ data.unformatQuotes ??= false
+ data.ttsAutoSpeech ??= false
+ data.translatorInputLanguage ??= 'auto'
+ data.falModel ??= 'fal-ai/flux/dev'
+ data.falLoraScale ??= 1
+ data.customCSS ??= ''
+ data.strictJsonSchema ??= true
+ data.statics ??= {
+ messages: 0,
+ imports: 0
+ }
+ data.customQuotes ??= false
+ data.customQuotesData ??= ['“','”','‘','’']
+ data.groupOtherBotRole ??= 'user'
changeLanguage(data.language)
DataBase.set(data)
}
@@ -508,6 +522,7 @@ export interface Database{
NAII2I:boolean
NAIREF:boolean
NAIImgConfig:NAIImgConfig
+ ttsAutoSpeech?:boolean
runpodKey:string
promptPreprocess:boolean
bias: [string, number][]
@@ -612,6 +627,7 @@ export interface Database{
emotionProcesser:'submodel'|'embedding',
showMenuChatList?:boolean,
translatorType:'google'|'deepl'|'none'|'llm'|'deeplX',
+ translatorInputLanguage?:string
NAIadventure?:boolean,
NAIappendName?:boolean,
deeplOptions:{
@@ -710,6 +726,30 @@ export interface Database{
comfyUiUrl: string
useLegacyGUI: boolean
claudeCachingExperimental: boolean
+ hideApiKey: boolean
+ unformatQuotes: boolean
+ enableDevTools: boolean
+ falToken: string
+ falModel: string
+ falLora: string
+ falLoraName: string
+ falLoraScale: number
+ moduleIntergration: string
+ customCSS: string
+ betaMobileGUI:boolean
+ jsonSchemaEnabled:boolean
+ jsonSchema:string
+ strictJsonSchema:boolean
+ extractJson:string
+ ai21Key:string
+ statics: {
+ messages: number
+ imports: number
+ }
+ customQuotes:boolean
+ customQuotesData?:[string, string, string, string]
+ groupTemplate?:string
+ groupOtherBotRole?:string
}
export interface customscript{
@@ -802,6 +842,27 @@ export interface character{
voice?: string
version?: string
}
+ gptSoVitsConfig?:{
+ url?:string
+ use_auto_path?:boolean
+ ref_audio_path?:string
+ use_long_audio?:boolean
+ ref_audio_data?: {
+ fileName:string
+ assetId:string
+ }
+ volume?:number
+ text_lang?: "auto" | "auto_yue" | "en" | "zh" | "ja" | "yue" | "ko" | "all_zh" | "all_ja" | "all_yue" | "all_ko"
+ text?:string
+ use_prompt?:boolean
+ prompt?:string | null
+ prompt_lang?: "auto" | "auto_yue" | "en" | "zh" | "ja" | "yue" | "ko" | "all_zh" | "all_ja" | "all_yue" | "all_ko"
+ top_p?:number
+ temperature?:number
+ speed?:number
+ top_k?:number
+ text_split_method?: "cut0" | "cut1" | "cut2" | "cut3" | "cut4" | "cut5"
+ }
supaMemory?:boolean
additionalAssets?:[string, string, string][]
ttsReadOnlyQuoted?:boolean
@@ -842,6 +903,8 @@ export interface character{
defaultVariables?:string
lowLevelAccess?:boolean
hideChatIcon?:boolean
+ lastInteraction?:number
+ translatorNote?:string
}
@@ -890,6 +953,7 @@ export interface groupChat{
defaultVariables?:string
lowLevelAccess?:boolean
hideChatIcon?:boolean
+ lastInteraction?:number
}
export interface botPreset{
@@ -939,6 +1003,16 @@ export interface botPreset{
useInstructPrompt?:boolean
customPromptTemplateToggle?:string
templateDefaultVariables?:string
+ moduleIntergration?:string
+ top_k?:number
+ instructChatTemplate?:string
+ JinjaTemplate?:string
+ jsonSchemaEnabled?:boolean
+ jsonSchema?:string
+ strictJsonSchema?:boolean
+ extractJson?:string
+ groupTemplate?:string
+ groupOtherBotRole?:string
}
@@ -1009,6 +1083,7 @@ export interface Chat{
modules?:string[]
id?:string
bindedPersona?:string
+ fmIndex?:number
}
export interface Message{
@@ -1018,6 +1093,8 @@ export interface Message{
chatId?:string
time?: number
generationInfo?: MessageGenerationInfo
+ name?:string
+ otherUser?:boolean
}
export interface MessageGenerationInfo{
@@ -1040,7 +1117,7 @@ interface AINsettings{
top_k:number
}
-interface OobaSettings{
+export interface OobaSettings{
max_new_tokens: number,
do_sample: boolean,
temperature: number,
@@ -1218,6 +1295,16 @@ export function saveCurrentPreset(){
useInstructPrompt: db.useInstructPrompt,
customPromptTemplateToggle: db.customPromptTemplateToggle ?? "",
templateDefaultVariables: db.templateDefaultVariables ?? "",
+ moduleIntergration: db.moduleIntergration ?? "",
+ top_k: db.top_k,
+ instructChatTemplate: db.instructChatTemplate,
+ JinjaTemplate: db.JinjaTemplate ?? '',
+ jsonSchemaEnabled:db.jsonSchemaEnabled??false,
+ jsonSchema:db.jsonSchema ?? '',
+ strictJsonSchema:db.strictJsonSchema ?? true,
+ extractJson:db.extractJson ?? '',
+ groupOtherBotRole: db.groupOtherBotRole ?? 'user',
+ groupTemplate: db.groupTemplate ?? '',
}
db.botPresets = pres
setDatabase(db)
@@ -1302,6 +1389,16 @@ export function setPreset(db:Database, newPres: botPreset){
db.useInstructPrompt = newPres.useInstructPrompt ?? false
db.customPromptTemplateToggle = newPres.customPromptTemplateToggle ?? ''
db.templateDefaultVariables = newPres.templateDefaultVariables ?? ''
+ db.moduleIntergration = newPres.moduleIntergration ?? ''
+ db.top_k = newPres.top_k ?? db.top_k
+ db.instructChatTemplate = newPres.instructChatTemplate ?? db.instructChatTemplate
+ db.JinjaTemplate = newPres.JinjaTemplate ?? db.JinjaTemplate
+ db.jsonSchemaEnabled = newPres.jsonSchemaEnabled ?? false
+ db.jsonSchema = newPres.jsonSchema ?? ''
+ db.strictJsonSchema = newPres.strictJsonSchema ?? true
+ db.extractJson = newPres.extractJson ?? ''
+ db.groupOtherBotRole = newPres.groupOtherBotRole ?? 'user'
+ db.groupTemplate = newPres.groupTemplate ?? ''
return db
}
@@ -1310,6 +1407,7 @@ import * as fflate from "fflate";
import type { OnnxModelFiles } from '../process/transformers';
import type { RisuModule } from '../process/modules';
import type { HypaV2Data } from '../process/memory/hypav2';
+import { decodeRPack, encodeRPack } from '../rpack/rpack_bg';
export async function downloadPreset(id:number, type:'json'|'risupreset'|'return' = 'json'){
saveCurrentPreset()
@@ -1334,8 +1432,10 @@ export async function downloadPreset(id:number, type:'json'|'risupreset'|'return
'risupreset'
)
}))
+
if(type === 'risupreset'){
- downloadFile(pres.name + "_preset.risupreset", buf)
+ const buf2 = await encodeRPack(buf)
+ downloadFile(pres.name + "_preset.risup", buf2)
}
else{
return {
@@ -1361,14 +1461,18 @@ export async function importPreset(f:{
data:Uint8Array
}|null = null){
if(!f){
- f = await selectSingleFile(["json", "preset", "risupreset"])
+ f = await selectSingleFile(["json", "preset", "risupreset", "risup"])
}
if(!f){
return
}
let pre:any
- if(f.name.endsWith('.risupreset')){
- const decoded = await decodeMsgpack(fflate.decompressSync(f.data))
+ if(f.name.endsWith('.risupreset') || f.name.endsWith('.risup')){
+ let data = f.data
+ if(f.name.endsWith('.risup')){
+ data = await decodeRPack(data)
+ }
+ const decoded = await decodeMsgpack(fflate.decompressSync(data))
console.log(decoded)
if((decoded.presetVersion === 0 || decoded.presetVersion === 2) && decoded.type === 'preset'){
pre = {...presetTemplate,...decodeMsgpack(Buffer.from(await decryptBuffer(decoded.preset ?? decoded.pres, 'risupreset')))}
diff --git a/src/ts/storage/globalApi.ts b/src/ts/storage/globalApi.ts
index 95093e42..07cf9243 100644
--- a/src/ts/storage/globalApi.ts
+++ b/src/ts/storage/globalApi.ts
@@ -1,18 +1,24 @@
-import { writeBinaryFile,BaseDirectory, readBinaryFile, exists, createDir, readDir, removeFile } from "@tauri-apps/api/fs"
-
+import {
+ writeFile,
+ BaseDirectory,
+ readFile,
+ exists,
+ mkdir,
+ readDir,
+ remove
+} from "@tauri-apps/plugin-fs"
import { changeFullscreen, checkNullish, findCharacterbyId, sleep } from "../util"
-import { convertFileSrc, invoke } from "@tauri-apps/api/tauri"
+import { convertFileSrc, invoke } from "@tauri-apps/api/core"
import { v4 as uuidv4, v4 } from 'uuid';
import { appDataDir, join } from "@tauri-apps/api/path";
import { get } from "svelte/store";
-import {open} from '@tauri-apps/api/shell'
+import {open} from '@tauri-apps/plugin-shell'
import { DataBase, loadedStore, setDatabase, type Database, defaultSdDataFunc } from "./database";
-import { appWindow } from "@tauri-apps/api/window";
+import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { checkRisuUpdate } from "../update";
-import { botMakerMode, selectedCharID } from "../stores";
-import { Body, ResponseType, fetch as TauriFetch } from "@tauri-apps/api/http";
+import { MobileGUI, botMakerMode, selectedCharID } from "../stores";
import { loadPlugins } from "../plugins/plugins";
-import { alertConfirm, alertError, alertNormal, alertNormalWait, alertSelect } from "../alert";
+import { alertConfirm, alertError, alertNormal, alertNormalWait, alertSelect, alertTOS, alertWait } from "../alert";
import { checkDriverInit, syncDrive } from "../drive/drive";
import { hasher } from "../parser";
import { characterURLImport, hubURL } from "../characterCards";
@@ -21,11 +27,11 @@ import { loadRisuAccountData } from "../drive/accounter";
import { decodeRisuSave, encodeRisuSave } from "./risuSave";
import { AutoStorage } from "./autoStorage";
import { updateAnimationSpeed } from "../gui/animation";
-import { updateColorScheme, updateTextTheme } from "../gui/colorscheme";
+import { updateColorScheme, updateTextThemeAndCSS } from "../gui/colorscheme";
import { saveDbKei } from "../kei/backup";
import { Capacitor, CapacitorHttp } from '@capacitor/core';
import * as CapFS from '@capacitor/filesystem'
-import { save } from "@tauri-apps/api/dialog";
+import { save } from "@tauri-apps/plugin-dialog";
import type { RisuModule } from "../process/modules";
import { listen } from '@tauri-apps/api/event'
import { registerPlugin } from '@capacitor/core';
@@ -35,13 +41,18 @@ import { removeDefaultHandler } from "src/main";
import { updateGuisize } from "../gui/guisize";
import { encodeCapKeySafe } from "./mobileStorage";
import { updateLorebooks } from "../characters";
+import { initMobileGesture } from "../hotkey";
+import { fetch as TauriHTTPFetch } from '@tauri-apps/plugin-http';
//@ts-ignore
-export const isTauri = !!window.__TAURI__
+export const isTauri = !!window.__TAURI_INTERNALS__
//@ts-ignore
export const isNodeServer = !!globalThis.__NODE__
export const forageStorage = new AutoStorage()
export const googleBuild = false
+export const isMobile = navigator.userAgent.match(/(iPad)|(iPhone)|(iPod)|(android)|(webOS)/i)
+
+const appWindow = isTauri ? getCurrentWebviewWindow() : null
interface fetchLog{
body:string
@@ -56,26 +67,6 @@ interface fetchLog{
let fetchLog:fetchLog[] = []
-async function writeBinaryFileFast(appPath: string, data: Uint8Array) {
- const secret = await invoke('get_http_secret') as string;
- const port = await invoke('get_http_port') as number;
-
- const apiUrl = `http://127.0.0.1:${port}/?path=${encodeURIComponent(appPath)}`;
-
- const response = await fetch(apiUrl, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/octet-stream',
- 'x-tauri-secret': secret
- },
- body: new Blob([data])
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-}
-
export async function downloadFile(name:string, dat:Uint8Array|ArrayBuffer|string) {
if(typeof(dat) === 'string'){
dat = Buffer.from(dat, 'utf-8')
@@ -92,7 +83,7 @@ export async function downloadFile(name:string, dat:Uint8Array|ArrayBuffer|strin
}
if(isTauri){
- await writeBinaryFile(name, data, {dir: BaseDirectory.Download})
+ await writeFile(name, data, {baseDir: BaseDirectory.Download})
}
else{
downloadURL(`data:png/image;base64,${Buffer.from(data).toString('base64')}`, name)
@@ -183,7 +174,7 @@ export async function getFileSrc(loc:string) {
return "/sw/img/" + encoded
}
else{
- const f:Uint8Array = await forageStorage.getItem(loc)
+ const f:Uint8Array = await forageStorage.getItem(loc) as unknown as Uint8Array
await fetch("/sw/register/" + encoded, {
method: "POST",
body: f
@@ -212,7 +203,7 @@ export async function getFileSrc(loc:string) {
ind = fileCache.origin.length
fileCache.origin.push(loc)
fileCache.res.push('loading')
- const f:Uint8Array = await forageStorage.getItem(loc)
+ const f:Uint8Array = await forageStorage.getItem(loc) as unknown as Uint8Array
fileCache.res[ind] = f
return `data:image/png;base64,${Buffer.from(f).toString('base64')}`
}
@@ -247,12 +238,12 @@ export async function readImage(data:string) {
if(appDataDirPath === ''){
appDataDirPath = await appDataDir();
}
- return await readBinaryFile(await join(appDataDirPath,data))
+ return await readFile(await join(appDataDirPath,data))
}
- return await readBinaryFile(data)
+ return await readFile(data)
}
else{
- return (await forageStorage.getItem(data) as Uint8Array)
+ return (await forageStorage.getItem(data) as unknown as Uint8Array)
}
}
@@ -281,7 +272,9 @@ export async function saveAsset(data:Uint8Array, customId:string = '', fileName:
fileExtension = fileName.split('.').pop()
}
if(isTauri){
- await writeBinaryFileFast(`assets/${id}.${fileExtension}`, data);
+ await writeFile(`assets/${id}.${fileExtension}`, data, {
+ baseDir: BaseDirectory.AppData
+ });
return `assets/${id}.${fileExtension}`
}
else{
@@ -302,10 +295,10 @@ export async function saveAsset(data:Uint8Array, customId:string = '', fileName:
*/
export async function loadAsset(id:string){
if(isTauri){
- return await readBinaryFile(id,{dir: BaseDirectory.AppData})
+ return await readFile(id,{baseDir: BaseDirectory.AppData})
}
else{
- return await forageStorage.getItem(id) as Uint8Array
+ return await forageStorage.getItem(id) as unknown as Uint8Array
}
}
@@ -336,8 +329,8 @@ export async function saveDb(){
}
if(!gotChannel){
gotChannel = true
- await alertNormalWait(language.activeTabChange)
- gotChannel = false
+ alertWait(language.activeTabChange)
+ location.reload()
}
}
}
@@ -358,8 +351,8 @@ export async function saveDb(){
db.saveTime = Math.floor(Date.now() / 1000)
if(isTauri){
const dbData = encodeRisuSave(db)
- await writeBinaryFileFast('database/database.bin', dbData);
- await writeBinaryFileFast(`database/dbbackup-${(Date.now()/100).toFixed()}.bin`, dbData);
+ await writeFile('database/database.bin', dbData, {baseDir: BaseDirectory.AppData});
+ await writeFile(`database/dbbackup-${(Date.now()/100).toFixed()}.bin`, dbData, {baseDir: BaseDirectory.AppData});
}
else{
if(!forageStorage.isAccount){
@@ -405,7 +398,7 @@ async function getDbBackups() {
return []
}
if(isTauri){
- const keys = await readDir('database', {dir: BaseDirectory.AppData})
+ const keys = await readDir('database', {baseDir: BaseDirectory.AppData})
let backups:number[] = []
for(const key of keys){
if(key.name.startsWith("dbbackup-")){
@@ -417,7 +410,7 @@ async function getDbBackups() {
backups.sort((a, b) => b - a)
while(backups.length > 20){
const last = backups.pop()
- await removeFile(`database/dbbackup-${last}.bin`,{dir: BaseDirectory.AppData})
+ await remove(`database/dbbackup-${last}.bin`,{baseDir: BaseDirectory.AppData})
}
return backups
}
@@ -452,27 +445,27 @@ export async function loadData() {
try {
if(isTauri){
appWindow.maximize()
- if(!await exists('', {dir: BaseDirectory.AppData})){
- await createDir('', {dir: BaseDirectory.AppData})
+ if(!await exists('', {baseDir: BaseDirectory.AppData})){
+ await mkdir('', {baseDir: BaseDirectory.AppData})
}
- if(!await exists('database', {dir: BaseDirectory.AppData})){
- await createDir('database', {dir: BaseDirectory.AppData})
+ if(!await exists('database', {baseDir: BaseDirectory.AppData})){
+ await mkdir('database', {baseDir: BaseDirectory.AppData})
}
- if(!await exists('assets', {dir: BaseDirectory.AppData})){
- await createDir('assets', {dir: BaseDirectory.AppData})
+ if(!await exists('assets', {baseDir: BaseDirectory.AppData})){
+ await mkdir('assets', {baseDir: BaseDirectory.AppData})
}
- if(!await exists('database/database.bin', {dir: BaseDirectory.AppData})){
- await writeBinaryFileFast('database/database.bin', encodeRisuSave({}));
+ if(!await exists('database/database.bin', {baseDir: BaseDirectory.AppData})){
+ await writeFile('database/database.bin', encodeRisuSave({}), {baseDir: BaseDirectory.AppData});
}
try {
- const decoded = decodeRisuSave(await readBinaryFile('database/database.bin',{dir: BaseDirectory.AppData}))
+ const decoded = decodeRisuSave(await readFile('database/database.bin',{baseDir: BaseDirectory.AppData}))
setDatabase(decoded)
} catch (error) {
const backups = await getDbBackups()
let backupLoaded = false
for(const backup of backups){
try {
- const backupData = await readBinaryFile(`database/dbbackup-${backup}.bin`,{dir: BaseDirectory.AppData})
+ const backupData = await readFile(`database/dbbackup-${backup}.bin`,{baseDir: BaseDirectory.AppData})
setDatabase(
decodeRisuSave(backupData)
)
@@ -490,7 +483,7 @@ export async function loadData() {
}
else{
- let gotStorage:Uint8Array = await forageStorage.getItem('database/database.bin')
+ let gotStorage:Uint8Array = await forageStorage.getItem('database/database.bin') as unknown as Uint8Array
if(checkNullish(gotStorage)){
gotStorage = encodeRisuSave({})
await forageStorage.setItem('database/database.bin', gotStorage)
@@ -505,7 +498,7 @@ export async function loadData() {
let backupLoaded = false
for(const backup of backups){
try {
- const backupData:Uint8Array = await forageStorage.getItem(`database/dbbackup-${backup}.bin`)
+ const backupData:Uint8Array = await forageStorage.getItem(`database/dbbackup-${backup}.bin`) as unknown as Uint8Array
setDatabase(
decodeRisuSave(backupData)
)
@@ -517,7 +510,7 @@ export async function loadData() {
}
}
if(await forageStorage.checkAccountSync()){
- let gotStorage:Uint8Array = await forageStorage.getItem('database/database.bin')
+ let gotStorage:Uint8Array = await forageStorage.getItem('database/database.bin') as unknown as Uint8Array
if(checkNullish(gotStorage)){
gotStorage = encodeRisuSave({})
await forageStorage.setItem('database/database.bin', gotStorage)
@@ -531,7 +524,7 @@ export async function loadData() {
let backupLoaded = false
for(const backup of backups){
try {
- const backupData:Uint8Array = await forageStorage.getItem(`database/dbbackup-${backup}.bin`)
+ const backupData:Uint8Array = await forageStorage.getItem(`database/dbbackup-${backup}.bin`) as unknown as Uint8Array
setDatabase(
decodeRisuSave(backupData)
)
@@ -568,11 +561,20 @@ export async function loadData() {
try {
await loadRisuAccountData()
} catch (error) {}
+ }
+ try {
+ //@ts-ignore
+ const isInStandaloneMode = (window.matchMedia('(display-mode: standalone)').matches) || (window.navigator.standalone) || document.referrer.includes('android-app://');
+ if(isInStandaloneMode){
+ await navigator.storage.persist()
+ }
+ } catch (error) {
+
}
await checkNewFormat()
const db = get(DataBase);
updateColorScheme()
- updateTextTheme()
+ updateTextThemeAndCSS()
updateAnimationSpeed()
updateHeightMode()
updateErrorHandling()
@@ -580,10 +582,21 @@ export async function loadData() {
if(db.botSettingAtStart){
botMakerMode.set(true)
}
+ if((db.betaMobileGUI && window.innerWidth <= 800) || import.meta.env.VITE_RISU_LITE === 'TRUE'){
+ initMobileGesture()
+ MobileGUI.set(true)
+ }
loadedStore.set(true)
selectedCharID.set(-1)
startObserveDom()
- saveDb()
+ saveDb()
+ if(import.meta.env.VITE_RISU_TOS === 'TRUE'){
+ alertTOS().then((a) => {
+ if(a === false){
+ location.reload()
+ }
+ })
+ }
} catch (error) {
alertError(`${error}`)
}
@@ -639,6 +652,7 @@ const knownHostes = ["localhost", "127.0.0.1", "0.0.0.0"];
*/
interface GlobalFetchArgs {
plainFetchForce?: boolean;
+ plainFetchDeforce?: boolean;
body?: any;
headers?: { [key: string]: string };
rawResponse?: boolean;
@@ -712,8 +726,8 @@ export async function globalFetch(url: string, arg: GlobalFetchArgs = {}): Promi
if (arg.abortSignal?.aborted) { return { ok: false, data: 'aborted', headers: {} }; }
- const urlHost = new URL(url).hostname;
- const forcePlainFetch = (knownHostes.includes(urlHost) && !isTauri) || db.usePlainFetch || arg.plainFetchForce;
+ const urlHost = new URL(url).hostname
+ const forcePlainFetch = ((knownHostes.includes(urlHost) && !isTauri) || db.usePlainFetch || arg.plainFetchForce) && !arg.plainFetchDeforce
if (knownHostes.includes(urlHost) && !isTauri && !isNodeServer) {
return { ok: false, headers: {}, data: 'You are trying local request on web version. This is not allowed due to browser security policy. Use the desktop version instead, or use a tunneling service like ngrok and set the CORS to allow all.' };
@@ -802,41 +816,19 @@ async function fetchWithPlainFetch(url: string, arg: GlobalFetchArgs): Promise} - The result of the fetch request.
*/
async function fetchWithTauri(url: string, arg: GlobalFetchArgs): Promise {
- const body = !arg.body ? null : arg.body instanceof URLSearchParams ? Body.text(arg.body.toString()) : Body.json(arg.body);
- const headers = arg.headers ?? {};
- const fetchPromise = TauriFetch(url, {
- body,
- method: arg.method ?? 'POST',
- headers,
- timeout: { secs: get(DataBase).timeOut, nanos: 0 },
- responseType: arg.rawResponse ? ResponseType.Binary : ResponseType.JSON,
- });
-
- let abortFn = () => {};
- const abortPromise = new Promise<"aborted">((res, rej) => {
- abortFn = () => res("aborted");
- arg.abortSignal?.addEventListener('abort', abortFn);
- });
-
- const result = await Promise.any([fetchPromise, abortPromise]);
- arg.abortSignal?.removeEventListener('abort', abortFn);
-
- if (result === 'aborted') {
- return { ok: false, data: 'aborted', headers: {} };
- }
-
- const data = arg.rawResponse ? new Uint8Array(result.data as number[]) : result.data;
- addFetchLogInGlobalFetch(data, result.ok, url, arg);
- return { ok: result.ok, data, headers: result.headers };
+ try {
+ const headers = { 'Content-Type': 'application/json', ...arg.headers };
+ const response = await TauriHTTPFetch(new URL(url), { body: JSON.stringify(arg.body), headers, method: arg.method ?? "POST", signal: arg.abortSignal });
+ const data = arg.rawResponse ? new Uint8Array(await response.arrayBuffer()) : await response.json();
+ const ok = response.status >= 200 && response.status < 300;
+ addFetchLogInGlobalFetch(data, ok, url, arg);
+ return { ok, data, headers: Object.fromEntries(response.headers) };
+ } catch (error) {
+
+ }
}
-/**
- * Performs a fetch request using Capacitor.
- *
- * @param {string} url - The URL to fetch.
- * @param {GlobalFetchArgs} arg - The arguments for the fetch request.
- * @returns {Promise} - The result of the fetch request.
- */
+// Decoupled globalFetch built-in function
async function fetchWithCapacitor(url: string, arg: GlobalFetchArgs): Promise {
const { body, headers = {}, rawResponse } = arg;
headers["Content-Type"] = body instanceof URLSearchParams ? "application/x-www-form-urlencoded" : "application/json";
@@ -992,7 +984,18 @@ export function getUnpargeables(db: Database, uptype: 'basename' | 'pure' = 'bas
}
}
- if (db.personas) {
+ if(db.modules){
+ for(const module of db.modules){
+ const assets = module.assets
+ if(assets){
+ for(const asset of assets){
+ addUnparge(asset[1])
+ }
+ }
+ }
+ }
+
+ if(db.personas){
db.personas.map((v) => {
addUnparge(v.icon);
});
@@ -1280,13 +1283,13 @@ async function pargeChunks(){
const unpargeable = getUnpargeables(db)
if(isTauri){
- const assets = await readDir('assets', {dir: BaseDirectory.AppData})
+ const assets = await readDir('assets', {baseDir: BaseDirectory.AppData})
for(const asset of assets){
const n = getBasename(asset.name)
if(unpargeable.includes(n)){
}
else{
- await removeFile(asset.path)
+ await remove(asset.name, {baseDir: BaseDirectory.AppData})
}
}
}
@@ -1401,7 +1404,7 @@ export class TauriWriter{
* @param {Uint8Array} data - The data to write.
*/
async write(data:Uint8Array) {
- await writeBinaryFile(this.path, data, {
+ await writeFile(this.path, data, {
append: !this.firstWrite
})
this.firstWrite = false
@@ -1864,7 +1867,7 @@ export async function fetchNative(url:string, arg:{
const data = nativeFetchData[fetchId].shift()
if(data.type === 'chunk'){
const chunk = Buffer.from(data.body, 'base64')
- controller.enqueue(chunk)
+ controller.enqueue(chunk as unknown as Uint8Array)
}
if(data.type === 'headers'){
resHeaders = data.body
@@ -2040,7 +2043,7 @@ export class BlankWriter{
export async function loadInternalBackup(){
- const keys = isTauri ? (await readDir('database', {dir: BaseDirectory.AppData})).map((v) => {
+ const keys = isTauri ? (await readDir('database', {baseDir: BaseDirectory.AppData})).map((v) => {
return v.name
}) : (await forageStorage.keys())
let internalBackups:string[] = []
@@ -2068,11 +2071,11 @@ export async function loadInternalBackup(){
const selectedBackup = internalBackups[alertResult]
const data = isTauri ? (
- await readBinaryFile('database/' + selectedBackup, {dir: BaseDirectory.AppData})
+ await readFile('database/' + selectedBackup, {baseDir: BaseDirectory.AppData})
) : (await forageStorage.getItem(selectedBackup))
setDatabase(
- decodeRisuSave(data)
+ decodeRisuSave(Buffer.from(data) as unknown as Uint8Array)
)
await alertNormal('Loaded backup')
diff --git a/src/ts/stores.ts b/src/ts/stores.ts
index 2b3a6f2d..5d75fc73 100644
--- a/src/ts/stores.ts
+++ b/src/ts/stores.ts
@@ -26,9 +26,12 @@ export const ViewBoxsize = writable({ width: 12 * 16, height: 12 * 16 }); // Def
export const settingsOpen = writable(false)
export const botMakerMode = writable(false)
export const moduleBackgroundEmbedding = writable('')
-
+export const openPresetList = writable(false)
+export const openPersonaList = writable(false)
+export const MobileGUI = writable(false)
+export const MobileGUIStack = writable(0)
+export const MobileSideBar = writable(0)
//optimization
-
export const CurrentCharacter = writable(null) as Writable
export const CurrentSimpleCharacter = writable(null) as Writable
export const CurrentChat = writable(null) as Writable
@@ -37,18 +40,37 @@ export const CurrentUserIcon = writable('') as Writable
export const CurrentShowMemoryLimit = writable(false) as Writable
export const ShowVN = writable(false)
export const SettingsMenuIndex = writable(-1)
-export const CurrentVariablePointer = writable({} as {[key:string]: string|number|boolean})
export const ReloadGUIPointer = writable(0)
export const OpenRealmStore = writable(false)
export const ShowRealmFrameStore = writable('')
export const PlaygroundStore = writable(0)
export const HideIconStore = writable(false)
export const UserIconProtrait = writable(false)
+export const CustomCSSStore = writable('')
+export const SafeModeStore = writable(false)
+export const MobileSearch = writable('')
+export const CharConfigSubMenu = writable(0)
+
let lastGlobalEnabledModules: string[] = []
let lastChatEnabledModules: string[] = []
let moduleHideIcon = false
let characterHideIcon = false
+
+CustomCSSStore.subscribe((css) => {
+ console.log(css)
+ const q = document.querySelector('#customcss')
+ if(q){
+ q.innerHTML = css
+ }
+ else{
+ const s = document.createElement('style')
+ s.id = 'customcss'
+ s.innerHTML = css
+ document.body.appendChild(s)
+ }
+})
+
function createSimpleCharacter(char:character|groupChat){
if((!char) || char.type === 'group'){
return null
@@ -208,13 +230,6 @@ async function preInit(){
if(getUserIcon() !== get(CurrentUserIcon)){
CurrentUserIcon.set(getUserIcon())
}
-
- const variablePointer = get(CurrentVariablePointer)
- const currentState = structuredClone(chat?.scriptstate)
-
- if(!isEqual(variablePointer, currentState)){
- CurrentVariablePointer.set(currentState)
- }
})
}
@@ -226,9 +241,7 @@ function onModuleUpdate(){
lastChatEnabledModules = []
}
- const m = getModules([
- ...lastGlobalEnabledModules, ...lastChatEnabledModules
- ])
+ const m = getModules()
let moduleHideIcon = false
let backgroundEmbedding = ''
diff --git a/src/ts/sync/multiuser.ts b/src/ts/sync/multiuser.ts
index e78a0c18..f370c504 100644
--- a/src/ts/sync/multiuser.ts
+++ b/src/ts/sync/multiuser.ts
@@ -1,23 +1,65 @@
import { v4 } from 'uuid';
-import { alertError, alertInput, alertNormal, alertWait } from '../alert';
-import { get } from 'svelte/store';
-import { DataBase, setDatabase, type character, saveImage } from '../storage/database';
-import { selectedCharID } from '../stores';
+import { alertError, alertInput, alertNormal, alertStore, alertWait } from '../alert';
+import { get, writable } from 'svelte/store';
+import { DataBase, setDatabase, type character, saveImage, type Chat } from '../storage/database';
+import { CurrentChat, selectedCharID } from '../stores';
import { findCharacterIndexbyId, sleep } from '../util';
import type { DataConnection, Peer } from 'peerjs';
import { readImage } from '../storage/globalApi';
+import { doingChat } from '../process';
async function importPeerJS(){
return await import('peerjs');
}
+interface ReciveFirst{
+ type: 'receive-char',
+ data: character
+}
+interface RequestFirst{
+ type: 'request-char'
+}
+interface ReciveAsset{
+ type: 'receive-asset',
+ id: string,
+ data: Uint8Array
+}
+interface RequestSync{
+ type: 'request-chat-sync',
+ id: string,
+ data: Chat
+}
+interface ReciveSync{
+ type: 'receive-chat',
+ data: Chat
+}
+interface RequestChatSafe{
+ type: 'request-chat-safe',
+ id: string
+}
+interface ResponseChatSafe{
+ type: 'response-chat-safe'
+ data: boolean,
+ id: string
+}
+interface RequestChat{
+ type: 'request-chat'
+}
+
+type ReciveData = ReciveFirst|RequestFirst|ReciveAsset|RequestSync|ReciveSync|RequestChatSafe|ResponseChatSafe|RequestChat
+
let conn:DataConnection
let peer:Peer
let connections:DataConnection[] = []
-let connectionOpen = false
+export let connectionOpen = false
+let requestChatSafeQueue = new Map()
+export let ConnectionOpenStore = writable(false)
+export let ConnectionIsHost = writable(false)
+export let RoomIdStore = writable('')
export async function createMultiuserRoom(){
//create a room with webrtc
+ ConnectionIsHost.set(true)
alertWait("Loading...")
const peerJS = await importPeerJS();
@@ -93,7 +135,96 @@ export async function createMultiuserRoom(){
return
}
char.chats[char.chatPage] = recivedChar.chats[0]
- sendPeerChar()
+ }
+ if(data.type === 'request-chat-sync'){
+ const db = get(DataBase)
+ const selectedCharId = get(selectedCharID)
+ const char = db.characters[selectedCharId]
+ char.chats[char.chatPage] = data.data
+ db.characters[selectedCharId] = char
+ latestSyncChat = data.data
+ setDatabase(db)
+
+ for(const connection of connections){
+ if(connection.connectionId === conn.connectionId){
+ continue
+ }
+ const rs:ReciveSync = {
+ type: 'receive-chat',
+ data: data.data
+ }
+ connection.send(rs)
+ }
+ }
+ if(data.type === 'request-chat'){
+ const db = get(DataBase)
+ const selectedCharId = get(selectedCharID)
+ const char = db.characters[selectedCharId]
+ const chat = char.chats[char.chatPage]
+ const rs:ReciveSync = {
+ type: 'receive-chat',
+ data: chat
+ }
+ conn.send(rs)
+ }
+ if(data.type === 'request-chat-safe'){
+ const queue = {
+ remaining: connections.length,
+ safe: true,
+ conn: conn
+ }
+ requestChatSafeQueue.set(data.id, queue)
+ for(const connection of connections){
+ if(connection.connectionId === conn.connectionId){
+ queue.remaining--
+ requestChatSafeQueue.set(data.id, queue)
+ continue
+ }
+ const rs:RequestChatSafe = {
+ type: 'request-chat-safe',
+ id: data.id
+ }
+ connection.send(rs)
+ }
+ if(queue.remaining === 0){
+ if(waitingMultiuserId === data.id){
+ waitingMultiuserId = ''
+ waitingMultiuserSafe = queue.safe
+ }
+ else if(queue.conn){
+ const rs:ResponseChatSafe = {
+ type: 'response-chat-safe',
+ data: queue.safe,
+ id: data.id
+ }
+ queue.conn.send(rs)
+ requestChatSafeQueue.delete(data.id)
+ }
+ }
+ }
+ if(data.type === 'response-chat-safe'){
+ const queue = requestChatSafeQueue.get(data.id)
+ if(queue){
+ queue.remaining--
+ if(!data.data){
+ queue.safe = false
+ }
+ if(queue.remaining === 0){
+ if(waitingMultiuserId === data.id){
+ waitingMultiuserId = ''
+ waitingMultiuserSafe = queue.safe
+ }
+ else if(queue.conn){
+ const rs:ResponseChatSafe = {
+ type: 'response-chat-safe',
+ data: queue.safe,
+ id: data.id
+ }
+ queue.conn.send(rs)
+ requestChatSafeQueue.delete(data.id)
+ }
+ }
+ }
}
});
@@ -111,28 +242,23 @@ export async function createMultiuserRoom(){
}
connectionOpen = true
- alertNormal("Room ID: " + roomId)
+ ConnectionOpenStore.set(true)
+ RoomIdStore.set(roomId)
+ alertStore.set({
+ type: 'none',
+ msg: ''
+ })
return
}
-interface ReciveFirst{
- type: 'receive-char',
- data: character
-}
-interface RequestFirst{
- type: 'request-char'
-}
-interface ReciveAsset{
- type: 'receive-asset',
- id: string,
- data: Uint8Array
-}
-
-type ReciveData = ReciveFirst|RequestFirst|ReciveAsset
+let waitingMultiuserId = ''
+let waitingMultiuserSafe = false
+let latestSyncChat:Chat|null = null
export async function joinMultiuserRoom(){
//join a room with webrtc
+ ConnectionIsHost.set(false)
alertWait("Loading...")
const peerJS = await importPeerJS();
peer = new peerJS.Peer(
@@ -145,6 +271,7 @@ export async function joinMultiuserRoom(){
let open = false
conn = peer.connect(roomId);
+ RoomIdStore.set(roomId)
conn.on('open', function() {
alertWait("Waiting for host to accept connection")
@@ -179,6 +306,32 @@ export async function joinMultiuserRoom(){
}
case 'receive-asset':{
saveImage(data.data, data.id)
+ break
+ }
+ case 'receive-chat':{
+ const db = get(DataBase)
+ const selectedCharId = get(selectedCharID)
+ const char = structuredClone(db.characters[selectedCharId])
+ char.chats[char.chatPage] = data.data
+ db.characters[selectedCharId] = char
+ latestSyncChat = data.data
+ setDatabase(db)
+ break
+ }
+ case 'request-chat-safe':{
+ const rs:ResponseChatSafe = {
+ type: 'response-chat-safe',
+ data: !get(doingChat) || data.id === waitingMultiuserId,
+ id: data.id
+ }
+ conn.send(rs)
+ break
+ }
+ case 'response-chat-safe':{
+ if(data.id === waitingMultiuserId){
+ waitingMultiuserId = ''
+ waitingMultiuserSafe = data.data
+ }
}
}
});
@@ -186,6 +339,7 @@ export async function joinMultiuserRoom(){
conn.on('close', function() {
alertError("Connection closed")
connectionOpen = false
+ ConnectionOpenStore.set(false)
selectedCharID.set(-1)
})
@@ -199,29 +353,78 @@ export async function joinMultiuserRoom(){
}
}
connectionOpen = true
+ ConnectionOpenStore.set(true)
alertNormal("Connected")
});
}
-export function sendPeerChar(){
+export async function peerSync(){
if(!connectionOpen){
return
}
+ await sleep(1)
+ const chat = get(CurrentChat)
+ latestSyncChat = chat
if(!conn){
// host user
for(const connection of connections){
connection.send({
- type: 'receive-char',
- data: get(DataBase).characters[get(selectedCharID)]
+ type: 'receive-chat',
+ data: chat
});
}
}
else{
conn.send({
- type: 'receive-char',
- data: get(DataBase).characters[get(selectedCharID)]
- });
+ type: 'request-chat-sync',
+ data: chat
+ } as RequestSync)
}
+}
+
+export async function peerSafeCheck() {
+ if(!connectionOpen){
+ return true
+ }
+ await sleep(500)
+ if(!conn){
+ waitingMultiuserId = v4()
+ requestChatSafeQueue.set(waitingMultiuserId, {
+ remaining: connections.length,
+ safe: true,
+ })
+ for(const connection of connections){
+ const rs:RequestChatSafe = {
+ type: 'request-chat-safe',
+ id: waitingMultiuserId
+ }
+ connection.send(rs)
+ }
+ while(waitingMultiuserId !== ''){
+ await sleep(100)
+ }
+ return waitingMultiuserSafe
+ }
+ else{
+ waitingMultiuserId = v4()
+ const rs:RequestChatSafe = {
+ type: 'request-chat-safe',
+ id: waitingMultiuserId
+ }
+ conn.send(rs)
+ while(waitingMultiuserId !== ''){
+ await sleep(100)
+ }
+ return waitingMultiuserSafe
+
+ }
+}
+
+export function peerRevertChat() {
+ if(!connectionOpen || !latestSyncChat){
+ return
+ }
+ CurrentChat.set(latestSyncChat)
}
\ No newline at end of file
diff --git a/src/ts/tokenizer.ts b/src/ts/tokenizer.ts
index 7afe4fa0..d0b6633f 100644
--- a/src/ts/tokenizer.ts
+++ b/src/ts/tokenizer.ts
@@ -1,12 +1,13 @@
import type { Tiktoken } from "@dqbd/tiktoken";
import type { Tokenizer } from "@mlc-ai/web-tokenizers";
-import { DataBase, type character } from "./storage/database";
+import { DataBase, type groupChat, type character, type Chat } from "./storage/database";
import { get } from "svelte/store";
import type { MultiModal, OpenAIChat } from "./process";
import { supportsInlayImage } from "./process/files/image";
import { risuChatParser } from "./parser";
import { tokenizeGGUFModel } from "./process/models/local";
import { globalFetch } from "./storage/globalApi";
+import { CurrentCharacter } from "./stores";
export const tokenizerList = [
@@ -18,6 +19,7 @@ export const tokenizerList = [
['llama3', 'Llama3'],
['novellist', 'Novellist'],
['gemma', 'Gemma'],
+ ['cohere', 'Cohere'],
] as const
export async function encode(data:string):Promise<(number[]|Uint32Array|Int32Array)>{
@@ -38,6 +40,8 @@ export async function encode(data:string):Promise<(number[]|Uint32Array|Int32Arr
return await tokenizeWebTokenizers(data, 'llama')
case 'gemma':
return await tokenizeWebTokenizers(data, 'gemma')
+ case 'cohere':
+ return await tokenizeWebTokenizers(data, 'cohere')
default:
// Add exception for gpt-4o tokenizers on reverse_proxy
if(db.proxyRequestModel?.startsWith('gpt4o') ||
@@ -342,4 +346,57 @@ export async function strongBan(data:string, bias:{[key:number]:number}) {
}
localStorage.setItem('strongBan_' + data, JSON.stringify(bias))
return bias
+}
+
+export async function getCharToken(char?:character|groupChat|null){
+ let persistant = 0
+ let dynamic = 0
+
+ if(!char){
+ const c = get(CurrentCharacter)
+ char = c
+ }
+ if(char.type === 'group'){
+ return {persistant:0, dynamic:0}
+ }
+
+ const basicTokenize = async (data:string) => {
+ data = data.replace(/{{char}}/g, char.name).replace(//g, char.name)
+ return await tokenize(data)
+ }
+
+ persistant += await basicTokenize(char.desc)
+ persistant += await basicTokenize(char.personality ?? '')
+ persistant += await basicTokenize(char.scenario ?? '')
+ for(const lore of char.globalLore){
+ let cont = lore.content.split('\n').filter((line) => {
+ if(line.startsWith('@@')){
+ return false
+ }
+ if(line === ''){
+ return false
+ }
+ return true
+ }).join('\n')
+ dynamic += await basicTokenize(cont)
+ }
+
+ return {persistant, dynamic}
+}
+
+export async function getChatToken(chat:Chat) {
+ let persistant = 0
+
+ const chatTokenizer = new ChatTokenizer(0, 'name')
+ const chatf = chat.message.map((d) => {
+ return {
+ role: d.role === 'user' ? 'user' : 'assistant',
+ content: d.data,
+ } as OpenAIChat
+ })
+ for(const chat of chatf){
+ persistant += await chatTokenizer.tokenizeChat(chat)
+ }
+
+ return persistant
}
\ No newline at end of file
diff --git a/src/ts/translator/translator.ts b/src/ts/translator/translator.ts
index 7640249b..b0b13b20 100644
--- a/src/ts/translator/translator.ts
+++ b/src/ts/translator/translator.ts
@@ -165,7 +165,7 @@ async function translateMain(text:string, arg:{from:string, to:string, host:stri
}
- const url = `https://${arg.host}/translate_a/single?client=gtx&dt=t&sl=${arg.from}&tl=${arg.to}&q=` + encodeURIComponent(text)
+ const url = `https://${arg.host}/translate_a/single?client=gtx&dt=t&sl=${db.translatorInputLanguage}&tl=${arg.to}&q=` + encodeURIComponent(text)
@@ -239,7 +239,7 @@ export async function translateHTML(html: string, reverse:boolean, charArg:simpl
return html
}
}
- if(db.translatorType === 'llm' && (!(isTauri || Capacitor.isNativePlatform()))){
+ if(db.translatorType === 'llm'){
const tr = db.translator || 'en'
return translateLLM(html, {to: tr})
}
@@ -456,14 +456,23 @@ async function translateLLM(text:string, arg:{to:string}){
})
const db = get(DataBase)
+ const charIndex = get(selectedCharID)
+ const currentChar = db.characters[charIndex]
+ let translatorNote
+ if (currentChar.type === "character") {
+ translatorNote = currentChar.translatorNote ?? ""
+ } else {
+ translatorNote = ""
+ }
+
let formated:OpenAIChat[] = []
let prompt = db.translatorPrompt || `You are a translator. translate the following html or text into {{slot}}. do not output anything other than the translation.`
- let parsedPrompt = parseChatML(prompt.replaceAll('{{slot}}', arg.to).replaceAll('{{solt::content}}', text))
+ let parsedPrompt = parseChatML(prompt.replaceAll('{{slot}}', arg.to).replaceAll('{{solt::content}}', text).replaceAll('{{slot::tnote}}', translatorNote))
if(parsedPrompt){
formated = parsedPrompt
}
else{
- prompt = prompt.replaceAll('{{slot}}', arg.to)
+ prompt = prompt.replaceAll('{{slot}}', arg.to).replaceAll('{{slot::tnote}}', translatorNote)
formated = [
{
'role': 'system',
diff --git a/src/ts/update.ts b/src/ts/update.ts
index ed94bd8d..85528a83 100644
--- a/src/ts/update.ts
+++ b/src/ts/update.ts
@@ -2,10 +2,9 @@ import { alertConfirm, alertWait } from "./alert";
import { language } from "../lang";
import { Capacitor } from "@capacitor/core";
import {
- checkUpdate,
- installUpdate,
-} from '@tauri-apps/api/updater'
-import { relaunch } from '@tauri-apps/api/process'
+ check,
+} from '@tauri-apps/plugin-updater'
+import { relaunch } from '@tauri-apps/plugin-process'
export async function checkRisuUpdate(){
@@ -14,12 +13,12 @@ export async function checkRisuUpdate(){
}
try {
- const checked = await checkUpdate()
- if(checked.shouldUpdate){
+ const checked = await check()
+ if(checked){
const conf = await alertConfirm(language.newVersion)
if(conf){
- alertWait(`Updating to ${checked.manifest.version}...`)
- await installUpdate()
+ alertWait(`Updating to ${checked.version}...`)
+ await checked.downloadAndInstall()
await relaunch()
}
}
diff --git a/src/ts/util.ts b/src/ts/util.ts
index 0e5975a3..e98eba82 100644
--- a/src/ts/util.ts
+++ b/src/ts/util.ts
@@ -2,12 +2,13 @@ import { get, writable, type Writable } from "svelte/store"
import type { Database, Message } from "./storage/database"
import { DataBase } from "./storage/database"
import { selectedCharID } from "./stores"
-import {open} from '@tauri-apps/api/dialog'
-import { readBinaryFile } from "@tauri-apps/api/fs"
+import {open} from '@tauri-apps/plugin-dialog'
+import { readFile } from "@tauri-apps/plugin-fs"
import { basename } from "@tauri-apps/api/path"
import { createBlankChar, getCharImage } from "./characters"
-import { appWindow } from '@tauri-apps/api/window';
+import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { isTauri } from "./storage/globalApi"
+const appWindow = isTauri ? getCurrentWebviewWindow() : null
export const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1
@@ -64,7 +65,7 @@ export async function selectSingleFile(ext:string[]){
} else if (selected === null) {
return null
} else {
- return {name: await basename(selected),data:await readBinaryFile(selected)}
+ return {name: await basename(selected),data:await readFile(selected)}
}
}
@@ -88,13 +89,13 @@ export async function selectMultipleFile(ext:string[]){
if (Array.isArray(selected)) {
let arr:{name:string, data:Uint8Array}[] = []
for(const file of selected){
- arr.push({name: await basename(file),data:await readBinaryFile(file)})
+ arr.push({name: await basename(file),data:await readFile(file)})
}
return arr
} else if (selected === null) {
return null
} else {
- return [{name: await basename(selected),data:await readBinaryFile(selected)}]
+ return [{name: await basename(selected),data:await readFile(selected)}]
}
}
@@ -471,13 +472,17 @@ export function parseMultilangString(data:string){
}
export const toLangName = (code:string) => {
- switch(code){
- case 'xx':{ //Special case for unknown language
- return 'Unknown Language'
- }
- default:{
- return new Intl.DisplayNames([code, 'en'], {type: 'language'}).of(code)
- }
+ try {
+ switch(code){
+ case 'xx':{ //Special case for unknown language
+ return 'Unknown Language'
+ }
+ default:{
+ return new Intl.DisplayNames([code, 'en'], {type: 'language'}).of(code)
+ }
+ }
+ } catch (error) {
+ return code
}
}
diff --git a/tsconfig.json b/tsconfig.json
index ca06bddc..80c16c31 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,9 +1,9 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
- "target": "ESNext",
+ "target": "ES2022",
"useDefineForClassFields": true,
- "module": "ESNext",
+ "module": "ES2022",
"resolveJsonModule": true,
"baseUrl": ".",
/**
diff --git a/version.json b/version.json
index 9c14a55a..a3afc201 100644
--- a/version.json
+++ b/version.json
@@ -1 +1 @@
-{"version":"124.2.2"}
\ No newline at end of file
+{"version":"136.0.1"}
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
index 360bd091..bde467ed 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -32,10 +32,7 @@ export default defineConfig({
host: '0.0.0.0', // listen on all addresses
port: 5174,
strictPort: true,
- hmr: {
- protocol: 'ws',
- port: 5184,
- },
+ hmr: false,
},
// to make use of `TAURI_DEBUG` and other env variables
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand