This commit is contained in:
hashcoko
2024-07-30 01:16:40 +09:00
73 changed files with 3735 additions and 1043 deletions

View File

@@ -6,7 +6,10 @@ import { Capacitor } from "@capacitor/core"
import { DataBase, type MessageGenerationInfo } from "./storage/database"
interface alertData{
type: 'error'| 'normal'|'none'|'ask'|'wait'|'selectChar'|'input'|'toast'|'wait2'|'markdown'|'select'|'login'|'tos'|'cardexport'|'requestdata'|'addchar'|'hypaV2'|'selectModule',
type: 'error'|'normal'|'none'|'ask'|'wait'|'selectChar'
|'input'|'toast'|'wait2'|'markdown'|'select'|'login'
|'tos'|'cardexport'|'requestdata'|'addchar'|'hypaV2'|'selectModule'
|'chatOptions',
msg: string,
submsg?: string
}
@@ -94,6 +97,21 @@ export async function alertAddCharacter() {
return get(alertStore).msg
}
export async function alertChatOptions() {
alertStore.set({
'type': 'chatOptions',
'msg': language.chatOptions
})
while(true){
if (get(alertStore).type === 'none'){
break
}
await sleep(10)
}
return parseInt(get(alertStore).msg)
}
export async function alertLogin(){
alertStore.set({
'type': 'login',

View File

@@ -693,7 +693,8 @@ async function importCharacterCardSpec(card:CharacterCardV2Risu|CharacterCardV3,
imported: true,
source: card?.data?.extensions?.risuai?.source ?? [],
ccAssets: ccAssets,
lowLevelAccess: risuext?.lowLevelAccess ?? false
lowLevelAccess: risuext?.lowLevelAccess ?? false,
defaultVariables: data?.extensions?.risuai?.defaultVariables ?? '',
}
if(card.spec === 'chara_card_v3'){
@@ -1051,7 +1052,7 @@ export function createBaseV3(char:character){
type: 'x-risu-asset',
uri: asset[1],
name: asset[0],
ext: asset[2] || 'unknown'
ext: asset[2] || 'png'
})
}
}
@@ -1062,7 +1063,7 @@ export function createBaseV3(char:character){
type: 'emotion',
uri: asset[1],
name: asset[0],
ext: 'unknown'
ext: 'png'
})
}
@@ -1148,7 +1149,8 @@ export function createBaseV3(char:character){
inlayViewScreen: char.inlayViewScreen,
newGenData: char.newGenData,
vits: {},
lowLevelAccess: char.lowLevelAccess ?? false
lowLevelAccess: char.lowLevelAccess ?? false,
defaultVariables: char.defaultVariables ?? '',
},
depth_prompt: char.depth_prompt
},

View File

@@ -3,7 +3,7 @@ import { DataBase, saveImage, setDatabase, type character, type Chat, defaultSdD
import { alertConfirm, alertError, alertNormal, alertSelect, alertStore, alertWait } from "./alert";
import { language } from "../lang";
import { decode as decodeMsgpack } from "msgpackr";
import { checkNullish, findCharacterbyId, selectMultipleFile, selectSingleFile, sleep } from "./util";
import { checkNullish, findCharacterbyId, getUserName, selectMultipleFile, selectSingleFile, sleep } from "./util";
import { v4 as uuidv4 } from 'uuid';
import { selectedCharID } from "./stores";
import { checkCharOrder, downloadFile, getFileSrc } from "./storage/globalApi";
@@ -197,7 +197,7 @@ export async function exportChat(page:number){
let i = 0
for(const v of chat.message){
alertWait(`Translating... ${i++}/${chat.message.length}`)
const name = v.saying ? findCharacterbyId(v.saying).name : v.role === 'char' ? char.name : anonymous ? '×××' : db.username
const name = v.saying ? findCharacterbyId(v.saying).name : v.role === 'char' ? char.name : anonymous ? '×××' : getUserName()
chatContentHTML += `<div class="chat">
<h2>${name}</h2>
<div>${await htmlChatParse(v.data)}</div>
@@ -268,7 +268,7 @@ export async function exportChat(page:number){
let i = 0
for(const v of chat.message){
alertWait(`Translating... ${i++}/${chat.message.length}`)
const name = v.saying ? findCharacterbyId(v.saying).name : v.role === 'char' ? char.name : anonymous ? '×××' : db.username
const name = v.saying ? findCharacterbyId(v.saying).name : v.role === 'char' ? char.name : anonymous ? '×××' : getUserName()
chatContentHTML += `<tr>
<td>${name}</td>
<td>${await htmlChatParse(v.data)}</td>
@@ -309,7 +309,7 @@ export async function exportChat(page:number){
return `--${findCharacterbyId(v.saying).name}\n${v.data}`
}
else{
return `--${v.role === 'char' ? char.name : db.username}\n${v.data}`
return `--${v.role === 'char' ? char.name : getUserName()}\n${v.data}`
}
}).join('\n\n')
@@ -409,7 +409,7 @@ export async function importChat(){
function formatTavernChat(chat:string, charName:string){
const db = get(DataBase)
return chat.replace(/<([Uu]ser)>|\{\{([Uu]ser)\}\}/g, db.username).replace(/((\{\{)|<)([Cc]har)(=.+)?((\}\})|>)/g, charName)
return chat.replace(/<([Uu]ser)>|\{\{([Uu]ser)\}\}/g, getUserName()).replace(/((\{\{)|<)([Cc]har)(=.+)?((\}\})|>)/g, charName)
}
export function characterFormatUpdate(index:number|character){

View File

@@ -95,9 +95,12 @@ export async function checkDriverInit() {
return false
}
} catch (error) {
location.search = ''
console.error(error)
alertError(`Backup Error: ${error}`)
const currentURL = new URL(location.href)
currentURL.search = ''
window.history.replaceState( {} , "", currentURL.href );
await sleep(100000)
return false
}
}

View File

@@ -1,4 +1,4 @@
import { get } from "svelte/store";
import { get, writable } from "svelte/store";
import { DataBase, setDatabase } from "../storage/database";
import { downloadFile } from "../storage/globalApi";
import { BufferToText, selectSingleFile } from "../util";
@@ -96,6 +96,8 @@ const colorShemes = {
} as const
export const ColorSchemeTypeStore = writable('dark' as 'dark'|'light')
export const colorSchemeList = Object.keys(colorShemes) as (keyof typeof colorShemes)[]
export function changeColorScheme(colorScheme: string){
@@ -114,7 +116,7 @@ export function updateColorScheme(){
let colorScheme = db.colorScheme
if(colorScheme == null){
colorScheme = defaultColorScheme
colorScheme = structuredClone(defaultColorScheme)
}
//set css variables
@@ -127,6 +129,7 @@ export function updateColorScheme(){
document.documentElement.style.setProperty("--risu-theme-textcolor2", colorScheme.textcolor2);
document.documentElement.style.setProperty("--risu-theme-darkborderc", colorScheme.darkBorderc);
document.documentElement.style.setProperty("--risu-theme-darkbutton", colorScheme.darkbutton);
ColorSchemeTypeStore.set(colorScheme.type)
}
export function exportColorScheme(){

View File

@@ -79,6 +79,8 @@ export function getModelName(name:string){
return 'Gemini Ultra Vision'
case 'claude-3-opus-20240229':
return 'Claude 3 Opus (20240229)'
case 'claude-3-5-sonnet-20240620':
return 'Claude 3.5 Sonnet (20240620)'
case 'claude-3-sonnet-20240229':
return 'Claude 3 Sonnet (20240229)'
case 'mistral-large-latest':

88
src/ts/mutex.ts Normal file
View File

@@ -0,0 +1,88 @@
/**
* A lock for synchronizing async operations.
* Use this to protect a critical section
* from getting modified by multiple async operations
* at the same time.
*/
export class Mutex {
/**
* When multiple operations attempt to acquire the lock,
* this queue remembers the order of operations.
*/
private _queue: {
resolve: (release: ReleaseFunction) => void
}[] = []
private _isLocked = false
/**
* Wait until the lock is acquired.
* @returns A function that releases the acquired lock.
*/
acquire() {
return new Promise<ReleaseFunction>((resolve) => {
this._queue.push({resolve})
this._dispatch()
});
}
/**
* Enqueue a function to be run serially.
*
* This ensures no other functions will start running
* until `callback` finishes running.
* @param callback Function to be run exclusively.
* @returns The return value of `callback`.
*/
async runExclusive<T>(callback: () => Promise<T>) {
const release = await this.acquire()
try {
return await callback()
} finally {
release()
}
}
/**
* Check the availability of the resource
* and provide access to the next operation in the queue.
*
* _dispatch is called whenever availability changes,
* such as after lock acquire request or lock release.
*/
private _dispatch() {
if (this._isLocked) {
// The resource is still locked.
// Wait until next time.
return
}
const nextEntry = this._queue.shift()
if (!nextEntry) {
// There is nothing in the queue.
// Do nothing until next dispatch.
return
}
// The resource is available.
this._isLocked = true
// and give access to the next operation
// in the queue.
nextEntry.resolve(this._buildRelease())
}
/**
* Build a release function for each operation
* so that it can release the lock after
* the operation is complete.
*/
private _buildRelease(): ReleaseFunction {
return () => {
// Each release function make
// the resource available again
this._isLocked = false
// and call dispatch.
this._dispatch()
}
}
}
type ReleaseFunction = () => void

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,12 @@
import { get } from "svelte/store"
import { DataBase, saveImage, setDatabase } from "./storage/database"
import { selectSingleFile, sleep } from "./util"
import { getUserName, selectSingleFile, sleep } from "./util"
import { alertError, alertNormal, alertStore } from "./alert"
import { downloadFile, readImage } from "./storage/globalApi"
import { language } from "src/lang"
import { reencodeImage } from "./process/files/image"
import { PngChunk } from "./pngChunk"
import { v4 } from "uuid"
export async function selectUserImg() {
const selected = await selectSingleFile(['png'])
@@ -19,19 +20,18 @@ export async function selectUserImg() {
db.personas[db.selectedPersona] = {
name: db.username,
icon: db.userIcon,
personaPrompt: db.personaPrompt
personaPrompt: db.personaPrompt,
id: v4()
}
setDatabase(db)
}
export function saveUserPersona() {
let db = get(DataBase)
db.personas[db.selectedPersona] = {
name: db.username,
icon: db.userIcon,
personaPrompt: db.personaPrompt,
largePortrait: db.personas[db.selectedPersona]?.largePortrait,
}
db.personas[db.selectedPersona].name=db.username
db.personas[db.selectedPersona].icon=db.userIcon,
db.personas[db.selectedPersona].personaPrompt=db.personaPrompt,
db.personas[db.selectedPersona].largePortrait=db.personas[db.selectedPersona]?.largePortrait,
setDatabase(db)
}
@@ -111,7 +111,8 @@ export async function importUserPersona(){
db.personas.push({
name: data.name,
icon: await saveImage(await reencodeImage(v.data)),
personaPrompt: data.personaPrompt
personaPrompt: data.personaPrompt,
id: v4()
})
setDatabase(db)
alertNormal(language.successImport)

View File

@@ -1,206 +0,0 @@
const excludesDat = ['<','>','{','}','[',']','(',')','-',':',';','…','—','','_','*','+','/','\\','|','!','?','.',',',' ']
const symbols = ['<','>','{','}','[',']','(',')','-',':',';','…','—','','_','*','+','/','\\','|','!','?','.',',',' ', '\n', '。', '、', '', '', '', '', '', '', '', '【', '】', '「', '」', '『', '』', '“', '”', '', '', '《', '》', '〈', '〉', '', '', '«', '»', '‟', '„']
const selfClosingTags = [
'br','hr','img','input','meta','link','base','area','col','command','embed','keygen','param','source','track','wbr',
//self closing tags defined by HTML5
'!',
//for doctype <!DOCTYPE html> and comment <!-- -->
'user', 'bot', 'char'
//special tags for user, bot, and char
]
const checkSelfClosingTag = (dat:string) => {
dat = dat.substring(0, 10) //we only need to check the first 10 characters, to avoid checking the whole string
dat = dat.toLowerCase() //we don't care about the case
for(const tag of selfClosingTags){
if(dat.startsWith(tag)){
return true
}
}
return false
}
export function risuFormater(dat:string){
const lines:[string,string][] = [['','']] // [type, content]
let htmlType = 0 // 0: not inside tag, 1: closing tag, 2: opening tag
for(let i=0;i<dat.length;i++){
const getLastLine = () => {
return lines[lines.length-1] ?? [
'not-found', ''
]
}
//html tag handling
if(dat[i] === '<' && getLastLine()[0] !== 'code-block'){
lines.push(['html-tag',''])
if(dat[i+1] === '/'){
htmlType = 1
}
else{
htmlType = 2
}
}
if(dat[i] === '>' && getLastLine()[0] === 'html-tag'){
const pop = lines.pop()
const tagAttr = pop[1].substring(1).trim()
if(htmlType === 1){
const pop2 = lines.pop() //probably html-inner
const chunk = pop2[1] + pop[1] + '>'
if(getLastLine()[0] === ''){
lines.push(['html-chunk',chunk])
lines.push(['',''])
}
else{
getLastLine()[1] += chunk
}
continue
}
else if(checkSelfClosingTag(tagAttr)){
const chunk = pop[1] + '>'
if(getLastLine()[0] === ''){
lines.push(['html-chunk',chunk])
lines.push(['',''])
}
else{
getLastLine()[1] += chunk
}
continue
}
else{
lines.push(['html-inner',pop[1]])
}
htmlType = 0
}
//code block handling
if(dat[i] === '`' && dat[i+1] === '`' && dat[i+2] === '`' && getLastLine()[0] === ''){
if(getLastLine()[0] === 'code-block'){
getLastLine()[1] += '```'
lines.push(['',''])
}
else{
lines.push(['code-block','```'])
}
i += 2
continue
}
if(dat[i] === '\n' && getLastLine()[0] === ''){
lines.push(['newline','\n'])
lines.push(['',''])
}
else if(lines[lines.length-1]){
lines[lines.length-1][1] += dat[i]
}
}
let result = ''
for(let i=0;i<lines.length;i++){
if(lines[i][0] !== ''){
result += lines[i][1]
continue
}
let line = lines[i][1] ??''
let isNumbered = false
let endMarked = false
if(excludesDat.includes(line[0]) || (line[1] === '.' && ['1','2','3','4','5','6','7','8','9'].includes(line[0]))){
isNumbered = true
}
if(line.endsWith('>') || line.endsWith('}') || line.startsWith('<')){
endMarked = true
}
if(isNumbered || endMarked){
result += line
continue
}
let depth = 0
let depthChunk:string[] = ['']
let depthChunkType:string[] = ['']
//spaces for detection
line = ' ' + line + ' '
const isNotCharacter = (t:string) => {
return symbols.includes(t)
}
for(let j=0;j<line.length;j++){
switch(line[j]){
case '"':
case '“':
case '”':{
if(depthChunkType[depth] === '"'){
depthChunkType.pop()
const pop = depthChunk.pop()
depth--
depthChunk[depth] += `<mark risu-mark="quote2">${pop}${line[j]}</mark>`
}
else{
depthChunkType.push('"')
depthChunk.push(line[j])
depth++
}
break
}
case "'":
case '':
case '':{
if(depthChunkType[depth] === "'"){
if(isNotCharacter(line[j-1]) || !isNotCharacter(line[j+1]) || (line[j-2] === 'i' && line[j-1] === 'n')){
//this is not a quote
depthChunk[depth] += line[j]
}
else{
depthChunkType.pop()
const pop = depthChunk.pop()
depth--
depthChunk[depth] += `<mark risu-mark="quote1">${pop}${line[j]}</mark>`
}
}
else{
if(!isNotCharacter(line[j-1]) || isNotCharacter(line[j+1])){
//this is not a quote
depthChunk[depth] += line[j]
}
else{
depthChunkType.push("'")
depthChunk.push(line[j])
depth++
}
}
break
}
default:{
depthChunk[depth] += line[j]
}
}
}
let lineResult = ''
while(depthChunk.length > 0){
lineResult = depthChunk.pop() + lineResult
}
if(lineResult.startsWith(' ')){
lineResult = lineResult.substring(1)
}
if(lineResult.endsWith(' ')){
lineResult = lineResult.substring(0,lineResult.length-1)
}
result += lineResult
}
return result.trim()
}

View File

@@ -3,6 +3,7 @@ import { HypaProcesser } from '../memory/hypamemory'
import type { OpenAIChat } from "..";
import { stringlizeChat } from "../stringlize";
import { get } from "svelte/store";
import { getUserName } from "src/ts/util";
export async function additionalInformations(char: character,chats:Chat,){
const processer = new HypaProcesser('MiniLM')
@@ -18,7 +19,7 @@ export async function additionalInformations(char: character,chats:Chat,){
if(!name){
if(chat.role === 'user'){
name = db.username
name = getUserName()
}
else{
name = char.name

View File

@@ -5,7 +5,7 @@ import { ChatTokenizer, tokenize, tokenizeNum } from "../tokenizer";
import { language } from "../../lang";
import { alertError } from "../alert";
import { loadLoreBookPrompt, loadLoreBookV3Prompt } from "./lorebook";
import { findCharacterbyId, getAuthorNoteDefaultText, isLastCharPunctuation, trimUntilPunctuation } from "../util";
import { findCharacterbyId, getAuthorNoteDefaultText, getPersonaPrompt, getUserName, isLastCharPunctuation, trimUntilPunctuation } from "../util";
import { requestChatData } from "./request";
import { stableDiff } from "./stableDiff";
import { processScript, processScriptFull, risuChatParser } from "./scripts";
@@ -27,6 +27,7 @@ import { addRerolls } from "./prereroll";
import { runImageEmbedding } from "./transformers";
import { hanuraiMemory } from "./memory/hanuraiMemory";
import { hypaMemoryV2 } from "./memory/hypav2";
import { runLuaEditTrigger } from "./lua";
export interface OpenAIChat{
role: 'system'|'user'|'assistant'|'function'
@@ -371,7 +372,7 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
if(db.personaPrompt){
unformated.personaPrompt.push({
role: 'system',
content: risuChatParser(db.personaPrompt, {chara: currentChar})
content: risuChatParser(getPersonaPrompt(), {chara: currentChar})
})
}
@@ -573,7 +574,7 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
}
}
const examples = exampleMessage(currentChar, db.username)
const examples = exampleMessage(currentChar, getUserName())
for(const example of examples){
currentTokens += await tokenizer.tokenizeChat(example)
@@ -633,7 +634,7 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
}
}
else if(msg.role === 'user'){
name = `${db.username}`
name = `${getUserName()}`
}
if(!msg.chatId){
msg.chatId = v4()
@@ -1046,6 +1047,8 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
data: formated
})
formated = await runLuaEditTrigger(currentChar, 'editRequest', formated)
//token rechecking
let inputTokens = 0

View File

@@ -1,4 +1,4 @@
import { getChatVar } from "../parser";
import { getChatVar, getGlobalChatVar } from "../parser";
function toRPN(expression:string) {
let outputQueue = '';
@@ -114,6 +114,13 @@ function executeRPNCalculation(text:string) {
return "0"
}
return parsed.toString()
}).replace(/\@([a-zA-Z0-9_]+)/g, (_, p1) => {
const v = getGlobalChatVar(p1)
const parsed = parseFloat(v)
if(isNaN(parsed)){
return "0"
}
return parsed.toString()
}).replace(/&&/g, '&').replace(/\|\|/g, '|').replace(/<=/g, '≤').replace(/>=/g, '≥').replace(/==/g, '=').replace(/null/gi, '0')
const expression = toRPN(text);
const evaluated = calculateRPN(expression);

664
src/ts/process/lua.ts Normal file
View File

@@ -0,0 +1,664 @@
import { getChatVar, 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 { alertError, alertInput, alertNormal } from "../alert";
import { HypaProcesser } from "./memory/hypamemory";
import { generateAIImage } from "./stableDiff";
import { writeInlayImage } from "./files/image";
import type { OpenAIChat } from ".";
import { requestChatData } from "./request";
import { v4 } from "uuid";
import { getModuleTriggers } from "./modules";
import { Mutex } from "../mutex";
let luaFactory:LuaFactory
let LuaSafeIds = new Set<string>()
let LuaEditDisplayIds = new Set<string>()
let LuaLowLevelIds = new Set<string>()
interface LuaEngineState {
code: string;
engine: LuaEngine;
mutex: Mutex;
}
let LuaEngines = new Map<string, LuaEngineState>()
export async function runLua(code:string, arg:{
char?:character|groupChat|simpleCharacterArgument,
chat?:Chat
setVar?: (key:string, value:string) => void,
getVar?: (key:string) => string,
lowLevelAccess?: boolean,
mode?: string,
data?: any
}){
const char = arg.char ?? get(CurrentCharacter)
const setVar = arg.setVar ?? setChatVar
const getVar = arg.getVar ?? getChatVar
const mode = arg.mode ?? 'manual'
const data = arg.data ?? {}
let chat = arg.chat ?? get(CurrentChat)
let stopSending = false
let lowLevelAccess = arg.lowLevelAccess ?? false
if(!luaFactory){
await makeLuaFactory()
}
let luaEngineState = LuaEngines.get(mode)
let wasEmpty = false
if (!luaEngineState) {
luaEngineState = {
code,
engine: await luaFactory.createEngine({injectObjects: true}),
mutex: new Mutex()
}
LuaEngines.set(mode, luaEngineState)
wasEmpty = true
}
return await luaEngineState.mutex.runExclusive(async () => {
if (wasEmpty || code !== luaEngineState.code) {
if (!wasEmpty)
luaEngineState.engine.global.close()
luaEngineState.engine = await luaFactory.createEngine({injectObjects: true})
const luaEngine = luaEngineState.engine
luaEngine.global.set('setChatVar', (id:string,key:string, value:string) => {
if(!LuaSafeIds.has(id) && !LuaEditDisplayIds.has(id)){
return
}
setVar(key, value)
})
luaEngine.global.set('getChatVar', (id:string,key:string) => {
if(!LuaSafeIds.has(id) && !LuaEditDisplayIds.has(id)){
return
}
return getVar(key)
})
luaEngine.global.set('stopChat', (id:string) => {
if(!LuaSafeIds.has(id)){
return
}
stopSending = true
})
luaEngine.global.set('alertError', (id:string, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
alertError(value)
})
luaEngine.global.set('alertNormal', (id:string, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
alertNormal(value)
})
luaEngine.global.set('alertInput', (id:string, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
return alertInput(value)
})
luaEngine.global.set('setChat', (id:string, index:number, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
const message = chat.message?.at(index)
if(message){
message.data = value
}
CurrentChat.set(chat)
})
luaEngine.global.set('setChatRole', (id:string, index:number, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
const message = chat.message?.at(index)
if(message){
message.role = value === 'user' ? 'user' : 'char'
}
CurrentChat.set(chat)
})
luaEngine.global.set('cutChat', (id:string, start:number, end:number) => {
if(!LuaSafeIds.has(id)){
return
}
chat.message = chat.message.slice(start,end)
CurrentChat.set(chat)
})
luaEngine.global.set('removeChat', (id:string, index:number) => {
if(!LuaSafeIds.has(id)){
return
}
chat.message.splice(index, 1)
CurrentChat.set(chat)
})
luaEngine.global.set('addChat', (id:string, role:string, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
let roleData:'user'|'char' = role === 'user' ? 'user' : 'char'
chat.message.push({role: roleData, data: value})
CurrentChat.set(chat)
})
luaEngine.global.set('insertChat', (id:string, index:number, role:string, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
let roleData:'user'|'char' = role === 'user' ? 'user' : 'char'
chat.message.splice(index, 0, {role: roleData, data: value})
CurrentChat.set(chat)
})
luaEngine.global.set('removeChat', (id:string, index:number) => {
if(!LuaSafeIds.has(id)){
return
}
chat.message.splice(index, 1)
CurrentChat.set(chat)
})
luaEngine.global.set('getChatLength', (id:string) => {
if(!LuaSafeIds.has(id)){
return
}
return chat.message.length
})
luaEngine.global.set('getFullChatMain', (id:string) => {
const data = JSON.stringify(chat.message.map((v) => {
return {
role: v.role,
data: v.data
}
}))
return data
})
luaEngine.global.set('setFullChatMain', (id:string, value:string) => {
const realValue = JSON.parse(value)
if(!LuaSafeIds.has(id)){
return
}
chat.message = realValue.map((v) => {
return {
role: v.role,
data: v.data
}
})
CurrentChat.set(chat)
})
luaEngine.global.set('logMain', (value:string) => {
console.log(JSON.parse(value))
})
luaEngine.global.set('reloadDisplay', (id:string) => {
if(!LuaSafeIds.has(id)){
return
}
ReloadGUIPointer.set(get(ReloadGUIPointer) + 1)
})
//Low Level Access
luaEngine.global.set('similarity', async (id:string, source:string, value:string[]) => {
if(!LuaLowLevelIds.has(id)){
return
}
const processer = new HypaProcesser('MiniLM')
await processer.addText(value)
return await processer.similaritySearch(source)
})
luaEngine.global.set('generateImage', async (id:string, value:string, negValue:string = '') => {
if(!LuaLowLevelIds.has(id)){
return
}
const gen = await generateAIImage(value, char as character, negValue, 'inlay')
if(!gen){
return 'Error: Image generation failed'
}
const imgHTML = new Image()
imgHTML.src = gen
const inlay = await writeInlayImage(imgHTML)
return `{{inlay::${inlay}}}`
})
luaEngine.global.set('LLMMain', async (id:string, promptStr:string) => {
let prompt:{
role: string,
content: string
}[] = JSON.parse(promptStr)
if(!LuaLowLevelIds.has(id)){
return
}
let promptbody:OpenAIChat[] = prompt.map((dict) => {
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 JSON.stringify({
success: false,
result: 'Error: ' + result.result
})
}
if(result.type === 'streaming' || result.type === 'multiline'){
return JSON.stringify({
success: false,
result: result.result
})
}
return JSON.stringify({
success: true,
result: result.result
})
})
luaEngine.global.set('simpleLLM', async (id:string, prompt:string) => {
if(!LuaLowLevelIds.has(id)){
return
}
const result = await requestChatData({
formated: [{
role: 'user',
content: prompt
}],
bias: {},
useStreaming: false,
noMultiGen: true,
}, 'model')
if(result.type === 'fail'){
return {
success: false,
result: 'Error: ' + result.result
}
}
if(result.type === 'streaming' || result.type === 'multiline'){
return {
success: false,
result: result.result
}
}
return {
success: true,
result: result.result
}
})
luaEngine.global.set('getName', async (id:string) => {
if(!LuaSafeIds.has(id)){
return
}
const db = get(DataBase)
const selectedChar = get(selectedCharID)
const char = db.characters[selectedChar]
return char.name
})
luaEngine.global.set('setName', async (id:string, name:string) => {
if(!LuaSafeIds.has(id)){
return
}
const db = get(DataBase)
const selectedChar = get(selectedCharID)
if(typeof name !== 'string'){
throw('Invalid data type')
}
db.characters[selectedChar].name = name
setDatabase(db)
})
luaEngine.global.set('setDescription', async (id:string, desc:string) => {
if(!LuaSafeIds.has(id)){
return
}
const db = get(DataBase)
const selectedChar = get(selectedCharID)
const char =db.characters[selectedChar]
if(typeof data !== 'string'){
throw('Invalid data type')
}
if(char.type === 'group'){
throw('Character is a group')
}
char.desc = desc
db.characters[selectedChar] = char
setDatabase(db)
})
luaEngine.global.set('setCharacterFirstMessage', async (id:string, data:string) => {
if(!LuaSafeIds.has(id)){
return
}
const db = get(DataBase)
const selectedChar = get(selectedCharID)
const char = db.characters[selectedChar]
if(typeof data !== 'string'){
return false
}
char.firstMessage = data
db.characters[selectedChar] = char
setDatabase(db)
return true
})
luaEngine.global.set('getCharacterFirstMessage', async (id:string) => {
if(!LuaSafeIds.has(id)){
return
}
const db = get(DataBase)
const selectedChar = get(selectedCharID)
const char = db.characters[selectedChar]
return char.firstMessage
})
luaEngine.global.set('getBackgroundEmbedding', async (id:string) => {
if(!LuaSafeIds.has(id)){
return
}
const db = get(DataBase)
const selectedChar = get(selectedCharID)
const char = db.characters[selectedChar]
return char.backgroundHTML
})
luaEngine.global.set('setBackgroundEmbedding', async (id:string, data:string) => {
if(!LuaSafeIds.has(id)){
return
}
const db = get(DataBase)
const selectedChar = get(selectedCharID)
if(typeof data !== 'string'){
return false
}
db.characters[selectedChar].backgroundHTML = data
setDatabase(db)
return true
})
await luaEngine.doString(luaCodeWarper(code))
luaEngineState.code = code
}
let accessKey = v4()
if(mode === 'editDisplay'){
LuaEditDisplayIds.add(accessKey)
}
else{
LuaSafeIds.add(accessKey)
if(lowLevelAccess){
LuaLowLevelIds.add(accessKey)
}
}
let res:any
const luaEngine = luaEngineState.engine
try {
switch(mode){
case 'input':{
const func = luaEngine.global.get('onInput')
if(func){
res = await func(accessKey)
}
break
}
case 'output':{
const func = luaEngine.global.get('onOutput')
if(func){
res = await func(accessKey)
}
break
}
case 'start':{
const func = luaEngine.global.get('onStart')
if(func){
res = await func(accessKey)
}
break
}
case 'editRequest':
case 'editDisplay':
case 'editInput':
case 'editOutput':{
const func = luaEngine.global.get('callListenMain')
if(func){
res = await func(mode, accessKey, JSON.stringify(data))
res = JSON.parse(res)
}
break
}
default:{
const func = luaEngine.global.get(mode)
if(func){
res = await func(accessKey)
}
break
}
}
if(res === false){
stopSending = true
}
} catch (error) {
console.error(error)
}
LuaSafeIds.delete(accessKey)
LuaLowLevelIds.delete(accessKey)
return {
stopSending, chat, res
}
})
}
async function makeLuaFactory(){
luaFactory = new LuaFactory()
async function mountFile(name:string){
let code = ''
for(let i = 0; i < 3; i++){
try {
const res = await fetch('/lua/' + name)
if(res.status >= 200 && res.status < 300){
code = await res.text()
break
}
} catch (error) {}
}
await luaFactory.mountFile(name,code)
}
await mountFile('json.lua')
}
function luaCodeWarper(code:string){
return `
json = require 'json'
function getFullChat(id)
return json.decode(getFullChatMain(id))
end
function setFullChat(id, value)
setFullChatMain(id, json.encode(value))
end
function log(value)
logMain(json.encode(value))
end
function LLM(id, prompt)
return json.decode(LLMMain(id, json.encode(prompt)):await())
end
local editRequestFuncs = {}
local editDisplayFuncs = {}
local editInputFuncs = {}
local editOutputFuncs = {}
function listenEdit(type, func)
if type == 'editRequest' then
editRequestFuncs[#editRequestFuncs + 1] = func
return
end
if type == 'editDisplay' then
editDisplayFuncs[#editDisplayFuncs + 1] = func
return
end
if type == 'editInput' then
editInputFuncs[#editInputFuncs + 1] = func
return
end
if type == 'editOutput' then
editOutputFuncs[#editOutputFuncs + 1] = func
return
end
throw('Invalid type')
end
function getState(id, name)
local escapedName = "__"..name
return json.decode(getChatVar(id, escapedName))
end
function setState(id, name, value)
local escapedName = "__"..name
setChatVar(id, escapedName, json.encode(value))
end
function async(callback)
return function(...)
local co = coroutine.create(callback)
local safe, result = coroutine.resume(co, ...)
return Promise.create(function(resolve, reject)
local checkresult
local step = function()
if coroutine.status(co) == "dead" then
local send = safe and resolve or reject
return send(result)
end
safe, result = coroutine.resume(co)
checkresult()
end
checkresult = function()
if safe and result == Promise.resolve(result) then
result:finally(step)
else
step()
end
end
checkresult()
end)
end
end
callListenMain = async(function(type, id, value)
local realValue = json.decode(value)
if type == 'editRequest' then
for _, func in ipairs(editRequestFuncs) do
realValue = func(id, realValue)
end
end
if type == 'editDisplay' then
for _, func in ipairs(editDisplayFuncs) do
realValue = func(id, realValue)
print(realValue)
end
end
if type == 'editInput' then
for _, func in ipairs(editInputFuncs) do
realValue = func(id, realValue)
end
end
if type == 'editOutput' then
for _, func in ipairs(editOutputFuncs) do
realValue = func(id, realValue)
end
end
return json.encode(realValue)
end)
${code}
`
}
export async function runLuaEditTrigger<T extends any>(char:character|groupChat|simpleCharacterArgument, mode:string, content:T):Promise<T>{
let data = content
switch(mode){
case 'editinput':
mode = 'editInput'
break
case 'editoutput':
mode = 'editOutput'
break
case 'editdisplay':
mode = 'editDisplay'
break
case 'editprocess':
return content
}
try {
const triggers = char.type === 'group' ? (getModuleTriggers()) : (char.triggerscript.map((v) => {
v.lowLevelAccess = false
return v
}).concat(getModuleTriggers()))
for(let trigger of triggers){
if(trigger?.effect?.[0]?.type === 'triggerlua'){
const runResult = await runLua(trigger.effect[0].code, {
char: char,
lowLevelAccess: false,
mode: mode,
data: data
})
data = runResult.res ?? data
}
}
return data
} catch (error) {
return content
}
}

View File

@@ -7,6 +7,7 @@ import { HypaProcesser } from "./hypamemory";
import { stringlizeChat } from "../stringlize";
import { globalFetch } from "src/ts/storage/globalApi";
import { runSummarizer } from "../transformers";
import { getUserName } from "src/ts/util";
export async function supaMemory(
chats:OpenAIChat[],
@@ -329,7 +330,7 @@ export async function supaMemory(
if((chunkSize + tokens) > maxChunkSize){
if(stringlizedChat === ''){
if(cont.role !== 'function' && cont.role !== 'system'){
stringlizedChat += `${cont.role === 'assistant' ? char.type === 'group' ? '' : char.name : db.username}: ${cont.content}\n\n`
stringlizedChat += `${cont.role === 'assistant' ? char.type === 'group' ? '' : char.name : getUserName()}: ${cont.content}\n\n`
spiceLen += 1
currentTokens -= tokens
chunkSize += tokens
@@ -338,7 +339,7 @@ export async function supaMemory(
lastId = cont.memo
break
}
stringlizedChat += `${cont.role === 'assistant' ? char.type === 'group' ? '' : char.name : db.username}: ${cont.content}\n\n`
stringlizedChat += `${cont.role === 'assistant' ? char.type === 'group' ? '' : char.name : getUserName()}: ${cont.content}\n\n`
spiceLen += 1
currentTokens -= tokens
chunkSize += tokens

View File

@@ -3,7 +3,7 @@ import type { OpenAIChat } from ".."
import { get } from "svelte/store"
import { globalFetch } from "src/ts/storage/globalApi"
import { alertError, alertInput, alertNormal, alertWait } from "src/ts/alert"
import { sleep } from "src/ts/util"
import { getUserName, sleep } from "src/ts/util"
export function stringlizeNAIChat(formated:OpenAIChat[], char:string, continued: boolean){
const db = get(DataBase)
@@ -34,7 +34,7 @@ export function stringlizeNAIChat(formated:OpenAIChat[], char:string, continued:
res += '> '
}
if(db.NAIappendName){
res += db.username + ": "
res += getUserName() + ": "
}
res += form.content
resultString.push(res)

View File

@@ -17,6 +17,8 @@ export interface RisuModule{
trigger?: triggerscript[]
id: string
lowLevelAccess?: boolean
hideIcon?: boolean
backgroundEmbedding?:string
}
export async function exportModule(module:RisuModule){

View File

@@ -1525,11 +1525,23 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
let lastChatPrompt = ''
let preamble = ''
const lastChat = formated[formated.length-1]
let lastChat = formated[formated.length-1]
if(lastChat.role === 'user'){
lastChatPrompt = lastChat.content
formated.pop()
}
else{
while(lastChat.role !== 'user'){
lastChat = formated.pop()
if(!lastChat){
return {
type: 'fail',
result: 'Cohere requires a user message to generate a response'
}
}
lastChatPrompt = (lastChat.role === 'user' ? '' : `${lastChat.role}: `) + '\n' + lastChat.content + lastChatPrompt
}
}
const firstChat = formated[0]
if(firstChat.role === 'system'){
@@ -1537,29 +1549,36 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
formated.shift()
}
//reformat chat
let body = {
message: lastChatPrompt,
chat_history: formated.map((v) => {
if(v.role === 'assistant'){
return {
role: 'CHATBOT',
content: v.content
message: v.content
}
}
if(v.role === 'system'){
return {
role: 'SYSTEM',
content: v.content
message: v.content
}
}
if(v.role === 'user'){
return {
role: 'USER',
content: v.content
message: v.content
}
}
return null
}).filter((v) => v !== null),
}).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,
@@ -1568,10 +1587,17 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
}
if(preamble){
// @ts-ignore
body.preamble = preamble
if(body.chat_history.length > 0){
// @ts-ignore
body.preamble = preamble
}
else{
body.message = `system: ${preamble}`
}
}
console.log(body)
const res = await globalFetch('https://api.cohere.com/v1/chat', {
method: "POST",
headers: {
@@ -1818,10 +1844,15 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model'
"anthropic.claude-v2:1",
"anthropic.claude-3-haiku-20240307-v1:0",
"anthropic.claude-3-sonnet-20240229-v1:0",
"anthropic.claude-3-5-sonnet-20240620-v1:0",
"anthropic.claude-3-opus-20240229-v1:0"
];
const awsModel = raiModel.includes("opus") ? modelIDs[4] : raiModel.includes("sonnet") ? modelIDs[3] : modelIDs[2];
const awsModel =
raiModel.includes("3-opus") ? modelIDs[5] :
raiModel.includes("3-5-sonnet") ? modelIDs[4] :
raiModel.includes("3-sonnet") ? modelIDs[3] :
modelIDs[2];
const url = `https://${host}/model/${awsModel}/invoke${stream ? "-with-response-stream" : ""}`
const params = {

View File

@@ -9,6 +9,7 @@ import { assetRegex, risuChatParser as risuChatParserOrg, type simpleCharacterAr
import { runCharacterJS } from "../plugins/embedscript";
import { getModuleRegexScripts } from "./modules";
import { HypaProcesser } from "./memory/hypamemory";
import { runLuaEditTrigger } from "./lua";
const dreg = /{{data}}/g
const randomness = /\|\|\|/g
@@ -56,6 +57,8 @@ export async function importRegex(){
}
}
let bestMatchCache = new Map<string, string>()
export async function processScriptFull(char:character|groupChat|simpleCharacterArgument, data:string, mode:ScriptMode, chatID = -1){
let db = get(DataBase)
let emoChanged = false
@@ -65,6 +68,7 @@ export async function processScriptFull(char:character|groupChat|simpleCharacter
mode,
data,
})
data = await runLuaEditTrigger(char, mode, data)
if(scripts.length === 0){
return {data, emoChanged}
}
@@ -193,7 +197,7 @@ export async function processScriptFull(char:character|groupChat|simpleCharacter
}
}
else{
data = risuChatParser(data.replace(reg, outScript))
data = risuChatParser(data.replace(reg, outScript), { chatID: chatID })
}
}
}
@@ -210,11 +214,18 @@ export async function processScriptFull(char:character|groupChat|simpleCharacter
for(const match of matches){
const type = match[1]
const assetName = match[2]
if(!assetNames.includes(assetName)){
const searched = await processer.similaritySearch(assetName)
const bestMatch = searched[0]
if(bestMatch){
data = data.replaceAll(match[0], `{{${type}::${bestMatch}}}`)
const cacheKey = char.chaId + '::' + assetName
if(type !== 'emotion' && type !== 'source'){
if(bestMatchCache.has(cacheKey)){
data = data.replaceAll(match[0], `{{${type}::${bestMatchCache.get(cacheKey)}}}`)
}
else if(!assetNames.includes(assetName)){
const searched = await processer.similaritySearch(assetName)
const bestMatch = searched[0]
if(bestMatch){
data = data.replaceAll(match[0], `{{${type}::${bestMatch}}}`)
bestMatchCache.set(cacheKey, bestMatch)
}
}
}
}

View File

@@ -413,6 +413,80 @@ export async function generateAIImage(genPrompt:string, currentChar:character, n
return returnSdData
}
if(db.sdProvider === 'comfy'){
const {workflow, posNodeID, posInputName, negNodeID, negInputName} = db.comfyConfig
const baseUrl = new URL(db.comfyUiUrl)
const createUrl = (pathname: string, params: Record<string, string> = {}) => {
const url = new URL(pathname, baseUrl)
url.search = new URLSearchParams(params).toString()
return url.toString()
}
const fetchWrapper = async (url: string, options = {}) => {
console.log(url)
const response = await globalFetch(url, options)
if (!response.ok) {
console.log(JSON.stringify(response.data))
throw new Error(JSON.stringify(response.data))
}
return response.data
}
try {
const prompt = JSON.parse(workflow)
prompt[posNodeID].inputs[posInputName] = genPrompt
prompt[negNodeID].inputs[negInputName] = neg
const { prompt_id: id } = await fetchWrapper(createUrl('/prompt'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: { 'prompt': prompt }
})
console.log(`prompt id: ${id}`)
let item
const startTime = Date.now()
const timeout = db.comfyConfig.timeout * 1000
while (!(item = (await (await fetch(createUrl('/history'), {
headers: { 'Content-Type': 'application/json' },
method: 'GET'})).json())[id])) {
console.log("Checking /history...")
if (Date.now() - startTime >= timeout) {
alertError("Error: Image generation took longer than expected.");
return false
}
await new Promise(r => setTimeout(r, 1000))
} // Check history until the generation is complete.
const genImgInfo = Object.values(item.outputs).flatMap((output: any) => output.images)[0];
const imgResponse = await fetch(createUrl('/view', {
filename: genImgInfo.filename,
subfolder: genImgInfo.subfolder,
type: genImgInfo.type
}), {
headers: { 'Content-Type': 'application/json' },
method: 'GET'})
const img64 = Buffer.from(await imgResponse.arrayBuffer()).toString('base64')
if(returnSdData === 'inlay'){
return `data:image/png;base64,${img64}`
}
else {
let charemotions = get(CharEmotion)
const img = `data:image/png;base64,${img64}`
const emos:[string, string,number][] = [[img, img, Date.now()]]
charemotions[currentChar.chaId] = emos
CharEmotion.set(charemotions)
}
return returnSdData
} catch (error) {
alertError(error)
return false
}
}
return ''
}
}

View File

@@ -1,6 +1,7 @@
import { get } from "svelte/store";
import type { OpenAIChat } from ".";
import { DataBase } from "../storage/database";
import { getUserName } from "../util";
export function multiChatReplacer(){
@@ -57,7 +58,7 @@ export function stringlizeChatOba(formated:OpenAIChat[], characterName:string, s
let name = form.name
if(form.role === 'user'){
prefix = appendWhitespace(suggesting ? assistantPrefix : userPrefix, seperator)
name ??= `${db.username}`
name ??= `${getUserName()}`
name += ': '
}
else if(form.role === 'assistant'){
@@ -80,7 +81,7 @@ export function stringlizeChatOba(formated:OpenAIChat[], characterName:string, s
if(!continued){
if(db.ooba.formating.useName){
if (suggesting){
resultString.push(appendWhitespace(assistantPrefix, seperator) + `${db.username}:\n` + db.autoSuggestPrefix)
resultString.push(appendWhitespace(assistantPrefix, seperator) + `${getUserName()}:\n` + db.autoSuggestPrefix)
} else {
resultString.push(assistantPrefix + `${characterName}:`)
}
@@ -173,17 +174,17 @@ export function getUnstringlizerChunks(formated:OpenAIChat[], char:string, mode:
chunks.push(`${char} `)
}
}
if(db.username){
charNames.push(db.username)
if(getUserName()){
charNames.push(getUserName())
if(mode === 'ain'){
chunks.push(`${db.username} `)
chunks.push(`${db.username} `)
chunks.push(`${getUserName()} `)
chunks.push(`${getUserName()} `)
}
else{
chunks.push(`${db.username}:`)
chunks.push(`${db.username}`)
chunks.push(`${db.username}: `)
chunks.push(`${db.username} `)
chunks.push(`${getUserName()}:`)
chunks.push(`${getUserName()}`)
chunks.push(`${getUserName()}: `)
chunks.push(`${getUserName()} `)
}
}
@@ -223,7 +224,7 @@ export function stringlizeAINChat(formated:OpenAIChat[], char:string, continued:
resultString.push(form.content)
}
else if(form.role === 'user'){
resultString.push(...formatToAIN(db.username, form.content))
resultString.push(...formatToAIN(getUserName(), form.content))
}
else if(form.name || form.role === 'assistant'){
resultString.push(...formatToAIN(form.name ?? char, form.content))
@@ -315,7 +316,7 @@ export function unstringlizeAIN(data:string,formated:OpenAIChat[], char:string =
}
}
else{
const role = (cont.character.trim() === db.username ? 'user' : 'char')
const role = (cont.character.trim() === getUserName() ? 'user' : 'char')
result.push([
role,
`${cont.content}`

View File

@@ -3,6 +3,7 @@ import type { OpenAIChat } from '..';
import { get } from 'svelte/store';
import { DataBase } from 'src/ts/storage/database';
import { CurrentCharacter } from 'src/ts/stores';
import { getUserName } from 'src/ts/util';
export const chatTemplates = {
'llama3': "{% set bos_token = '<|begin_of_text|>' %}{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}",
@@ -93,6 +94,6 @@ export const applyChatTemplate = (messages:OpenAIChat[]) => {
"messages": formatedMessages,
"add_generation_prompt": true,
"risu_char": currentChar.name,
"risu_user": db.username
"risu_user": getUserName()
})
}

View File

@@ -1,4 +1,4 @@
import { risuChatParser } from "../parser";
import { parseChatML, risuChatParser, risuCommandParser } from "../parser";
import { DataBase, type Chat, type character } from "../storage/database";
import { tokenize } from "../tokenizer";
import { getModuleTriggers } from "./modules";
@@ -12,6 +12,8 @@ import { HypaProcesser } from "./memory/hypamemory";
import { requestChatData } from "./request";
import { generateAIImage } from "./stableDiff";
import { writeInlayImage } from "./files/image";
import { runLua } from "./lua";
export interface triggerscript{
comment: string;
@@ -23,7 +25,7 @@ export interface triggerscript{
export type triggerCondition = triggerConditionsVar|triggerConditionsExists|triggerConditionsChatIndex
export type triggerEffect = triggerEffectCutChat|triggerEffectModifyChat|triggerEffectImgGen|triggerEffectRegex|triggerEffectRunLLM|triggerEffectCheckSimilarity|triggerEffectSendAIprompt|triggerEffectShowAlert|triggerEffectSetvar|triggerEffectSystemPrompt|triggerEffectImpersonate|triggerEffectCommand|triggerEffectStop|triggerEffectRunTrigger
export type triggerEffect = triggerCode|triggerEffectCutChat|triggerEffectModifyChat|triggerEffectImgGen|triggerEffectRegex|triggerEffectRunLLM|triggerEffectCheckSimilarity|triggerEffectSendAIprompt|triggerEffectShowAlert|triggerEffectSetvar|triggerEffectSystemPrompt|triggerEffectImpersonate|triggerEffectCommand|triggerEffectStop|triggerEffectRunTrigger
export type triggerConditionsVar = {
type:'var'|'value'
@@ -32,6 +34,11 @@ export type triggerConditionsVar = {
operator:'='|'!='|'>'|'<'|'>='|'<='|'null'|'true'
}
export type triggerCode = {
type: 'triggercode'|'triggerlua',
code: string
}
export type triggerConditionsChatIndex = {
type:'chatindex'
value:string
@@ -197,7 +204,10 @@ export async function runTrigger(char:character,mode:triggerMode, arg:{
for(const trigger of triggers){
if(arg.manualName){
if(trigger.effect[0]?.type === 'triggercode' || trigger.effect[0]?.type === 'triggerlua'){
//
}
else if(arg.manualName){
if(trigger.comment !== arg.manualName){
continue
}
@@ -238,22 +248,22 @@ export async function runTrigger(char:character,mode:triggerMode, arg:{
}
break
case '>':
if(Number(varValue) > Number(conditionValue)){
if(Number(varValue) <= Number(conditionValue)){
pass = false
}
break
case '<':
if(Number(varValue) < Number(conditionValue)){
pass = false
}
break
case '>=':
if(Number(varValue) >= Number(conditionValue)){
pass = false
}
break
case '>=':
if(Number(varValue) < Number(conditionValue)){
pass = false
}
break
case '<=':
if(Number(varValue) <= Number(conditionValue)){
if(Number(varValue) > Number(conditionValue)){
pass = false
}
break
@@ -393,6 +403,7 @@ export async function runTrigger(char:character,mode:triggerMode, arg:{
case 'input':{
const val = await alertInput(effectValue)
setVar(inputVar, val)
break;
}
case 'select':{
const val = await alertSelect(effectValue.split('§'))
@@ -416,41 +427,10 @@ export async function runTrigger(char:character,mode:triggerMode, arg:{
}
const effectValue = risuChatParser(effect.value,{chara:char})
const varName = effect.inputVar
let promptbody:OpenAIChat[] = []
let currentRole:'user'|'assistant'|'system'
const splited = effectValue.split('\n')
for(let i = 0; i < splited.length; i++){
const line = splited[i]
if(line.startsWith('@@role ')){
const role = line.split(' ')[1]
switch(role){
case 'user':
case 'assistant':
case 'system':
currentRole = role
break
default:
currentRole = 'system'
break
}
promptbody.push({role: currentRole, content: ''})
continue
}
else if(promptbody.length === 0){
promptbody.push({role: 'system', content: line})
}
else{
promptbody[promptbody.length - 1].content += line
}
let promptbody:OpenAIChat[] = parseChatML(effectValue)
if(!promptbody){
promptbody = [{role:'user', content:effectValue}]
}
promptbody = promptbody.map((e) => {
e.content = e.content.trim()
return e
}).filter((e) => e.content.length > 0)
const result = await requestChatData({
formated: promptbody,
bias: {},
@@ -518,6 +498,35 @@ 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,
mode: mode === 'manual' ? arg.manualName : mode,
setVar: setVar,
getVar: getVar,
char: char,
chat: chat,
})
if(triggerCodeResult.stopSending){
stopSending = true
}
chat = triggerCodeResult.chat
break
}
}
}
}

View File

@@ -1,4 +1,4 @@
import { get } from "svelte/store"
import { get, writable } from "svelte/store"
import { DataBase } from "./database"
import { hubURL } from "../characterCards"
import localforage from "localforage"
@@ -7,6 +7,10 @@ import { forageStorage, getUnpargeables, replaceDbResources } from "./globalApi"
import { encodeRisuSave } from "./risuSave"
import { v4 } from "uuid"
export const AccountWarning = writable('')
let seenWarnings:string[] = []
export class AccountStorage{
auth:string
usingSync:boolean
@@ -25,10 +29,23 @@ export class AccountStorage{
'X-Format': 'nocheck'
}
})
if(da.headers.get('Content-Type') === 'application/json'){
const json = (await da.json())
if(json?.warning){
if(!seenWarnings.includes(json.warning)){
seenWarnings.push(json.warning)
AccountWarning.set(json.warning)
}
}
}
if(da.status === 304){
return key
}
if(da.status === 403){
if(da.headers.get('x-risu-status') === 'warn'){
return
}
localStorage.setItem("fallbackRisuToken",await alertLogin())
this.checkAuth()
}

View File

@@ -1,3 +1,8 @@
export const DataBase = writable({} as any as Database)
export const loadedStore = writable(false)
export let appVer = "122.1.2"
export let webAppSubVer = ''
import { get, writable } from 'svelte/store';
import { checkNullish, decryptBuffer, encryptBuffer, selectSingleFile } from '../util';
import { changeLanguage, language } from '../../lang';
@@ -12,11 +17,6 @@ import { defaultColorScheme, type ColorScheme } from '../gui/colorscheme';
import type { PromptItem, PromptSettings } from '../process/prompt';
import type { OobaChatCompletionRequestParams } from '../model/ooba';
export const DataBase = writable({} as any as Database)
export const loadedStore = writable(false)
export let appVer = "115.0.1"
export let webAppSubVer = ''
export function setDatabase(data:Database){
if(checkNullish(data.characters)){
data.characters = []
@@ -419,6 +419,16 @@ export function setDatabase(data:Database){
data.stabilityModel ??= 'sd3-large'
data.stabllityStyle ??= ''
data.legacyTranslation ??= false
data.comfyUiUrl ??= 'http://localhost:8188'
data.comfyConfig ??= {
workflow: '',
posNodeID: '',
posInputName: 'text',
negNodeID: '',
negInputName: 'text',
timeout: 30
}
changeLanguage(data.language)
DataBase.set(data)
}
@@ -585,6 +595,7 @@ export interface Database{
name:string
icon:string
largePortrait?:boolean
id?:string
}[]
assetWidth:number
animationSpeed:number
@@ -695,6 +706,8 @@ export interface Database{
stabilityKey: string
stabllityStyle: string
legacyTranslation: boolean
comfyConfig: ComfyConfig
comfyUiUrl: string
}
export interface customscript{
@@ -826,6 +839,7 @@ export interface character{
}>
defaultVariables?:string
lowLevelAccess?:boolean
hideChatIcon?:boolean
}
@@ -873,6 +887,7 @@ export interface groupChat{
nickname?:string
defaultVariables?:string
lowLevelAccess?:boolean
hideChatIcon?:boolean
}
export interface botPreset{
@@ -965,6 +980,16 @@ interface NAIImgConfig{
InfoExtracted:number,
RefStrength:number
}
interface ComfyConfig{
workflow:string,
posNodeID: string,
posInputName:string,
negNodeID: string,
negInputName:string,
timeout: number
}
export type FormatingOrderItem = 'main'|'jailbreak'|'chats'|'lorebook'|'globalNote'|'authorNote'|'lastChat'|'description'|'postEverything'|'personaPrompt'
export interface Chat{
@@ -981,6 +1006,7 @@ export interface Chat{
scriptstate?:{[key:string]:string|number|boolean}
modules?:string[]
id?:string
bindedPersona?:string
}
export interface Message{
@@ -1299,9 +1325,9 @@ export async function downloadPreset(id:number, type:'json'|'risupreset'|'return
}
else if(type === 'risupreset' || type === 'return'){
const buf = fflate.compressSync(encodeMsgpack({
presetVersion: 0,
presetVersion: 2,
type: 'preset',
pres: await encryptBuffer(
preset: await encryptBuffer(
encodeMsgpack(pres),
'risupreset'
)
@@ -1342,8 +1368,8 @@ export async function importPreset(f:{
if(f.name.endsWith('.risupreset')){
const decoded = await decodeMsgpack(fflate.decompressSync(f.data))
console.log(decoded)
if(decoded.presetVersion === 0 && decoded.type === 'preset'){
pre = {...presetTemplate,...decodeMsgpack(Buffer.from(await decryptBuffer(decoded.pres, 'risupreset')))}
if((decoded.presetVersion === 0 || decoded.presetVersion === 2) && decoded.type === 'preset'){
pre = {...presetTemplate,...decodeMsgpack(Buffer.from(await decryptBuffer(decoded.preset ?? decoded.pres, 'risupreset')))}
}
}
else{
@@ -1484,4 +1510,4 @@ export async function importPreset(f:{
pre.name ??= "Imported"
db.botPresets.push(pre)
setDatabase(db)
}
}

View File

@@ -1,4 +1,5 @@
import { writeBinaryFile,BaseDirectory, readBinaryFile, exists, createDir, readDir, removeFile } from "@tauri-apps/api/fs"
import { changeFullscreen, checkNullish, findCharacterbyId, sleep } from "../util"
import { convertFileSrc, invoke } from "@tauri-apps/api/tauri"
import { v4 as uuidv4, v4 } from 'uuid';
@@ -11,7 +12,7 @@ import { checkRisuUpdate } from "../update";
import { botMakerMode, selectedCharID } from "../stores";
import { Body, ResponseType, fetch as TauriFetch } from "@tauri-apps/api/http";
import { loadPlugins } from "../plugins/plugins";
import { alertConfirm, alertError, alertNormal, alertNormalWait } from "../alert";
import { alertConfirm, alertError, alertNormal, alertNormalWait, alertSelect } from "../alert";
import { checkDriverInit, syncDrive } from "../drive/drive";
import { hasher } from "../parser";
import { characterURLImport, hubURL } from "../characterCards";
@@ -55,12 +56,26 @@ interface fetchLog{
let fetchLog:fetchLog[] = []
/**
* Downloads a file with the given name and data.
*
* @param {string} name - The name of the file to be downloaded.
* @param {Uint8Array|ArrayBuffer|string} dat - The data of the file to be downloaded.
*/
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')
@@ -266,7 +281,7 @@ export async function saveAsset(data:Uint8Array, customId:string = '', fileName:
fileExtension = fileName.split('.').pop()
}
if(isTauri){
await writeBinaryFile(`assets/${id}.${fileExtension}`, data ,{dir: BaseDirectory.AppData})
await writeBinaryFileFast(`assets/${id}.${fileExtension}`, data);
return `assets/${id}.${fileExtension}`
}
else{
@@ -341,13 +356,14 @@ export async function saveDb(){
changed = false
let db = get(DataBase)
db.saveTime = Math.floor(Date.now() / 1000)
const dbData = encodeRisuSave(db)
if(isTauri){
await writeBinaryFile('database/database.bin', dbData, {dir: BaseDirectory.AppData})
await writeBinaryFile(`database/dbbackup-${(Date.now()/100).toFixed()}.bin`, dbData, {dir: BaseDirectory.AppData})
const dbData = encodeRisuSave(db)
await writeBinaryFileFast('database/database.bin', dbData);
await writeBinaryFileFast(`database/dbbackup-${(Date.now()/100).toFixed()}.bin`, dbData);
}
else{
if(!forageStorage.isAccount){
const dbData = encodeRisuSave(db)
await forageStorage.setItem('database/database.bin', dbData)
await forageStorage.setItem(`database/dbbackup-${(Date.now()/100).toFixed()}.bin`, dbData)
}
@@ -386,7 +402,7 @@ export async function saveDb(){
async function getDbBackups() {
let db = get(DataBase)
if(db?.account?.useSync){
return
return []
}
if(isTauri){
const keys = await readDir('database', {dir: BaseDirectory.AppData})
@@ -446,14 +462,11 @@ export async function loadData() {
await createDir('assets', {dir: BaseDirectory.AppData})
}
if(!await exists('database/database.bin', {dir: BaseDirectory.AppData})){
await writeBinaryFile('database/database.bin',
encodeRisuSave({})
,{dir: BaseDirectory.AppData})
await writeBinaryFileFast('database/database.bin', encodeRisuSave({}));
}
try {
setDatabase(
decodeRisuSave(await readBinaryFile('database/database.bin',{dir: BaseDirectory.AppData}))
)
const decoded = decodeRisuSave(await readBinaryFile('database/database.bin',{dir: BaseDirectory.AppData}))
setDatabase(decoded)
} catch (error) {
const backups = await getDbBackups()
let backupLoaded = false
@@ -483,10 +496,11 @@ export async function loadData() {
await forageStorage.setItem('database/database.bin', gotStorage)
}
try {
setDatabase(
decodeRisuSave(gotStorage)
)
const decoded = decodeRisuSave(gotStorage)
console.log(decoded)
setDatabase(decoded)
} catch (error) {
console.error(error)
const backups = await getDbBackups()
let backupLoaded = false
for(const backup of backups){
@@ -1076,23 +1090,24 @@ async function checkNewFormat(): Promise<void> {
if (v.lorebook) {
v.lorebook = updateLorebooks(v.lorebook);
}
return v;
});
return v
})
if (!db.formatversion) {
/**
* Checks and updates the path of a given data string.
*
* @param {string} data - The data string to be checked and updated.
* @returns {string} - The updated data string with the correct path.
*/
function checkParge(data: string): string {
if (data.startsWith('assets') || (data.length < 3)) {
return data;
} else {
const d = 'assets/' + (data.replace(/\\/g, '/').split('assets/')[1]);
if (!d) {
return data;
db.personas = (db.personas ?? []).map((v) => {
v.id ??= uuidv4()
return v
})
if(!db.formatversion){
function checkParge(data:string){
if(data.startsWith('assets') || (data.length < 3)){
return data
}
else{
const d = 'assets/' + (data.replace(/\\/g, '/').split('assets/')[1])
if(!d){
return data
}
return d;
}
@@ -2021,4 +2036,47 @@ export class BlankWriter{
async end(){
//do nothing, just to make compatible with other writer
}
}
export async function loadInternalBackup(){
const keys = isTauri ? (await readDir('database', {dir: BaseDirectory.AppData})).map((v) => {
return v.name
}) : (await forageStorage.keys())
let internalBackups:string[] = []
for(const key of keys){
if(key.includes('dbbackup-')){
internalBackups.push(key)
}
}
const selectOptions = [
'Cancel',
...(internalBackups.map((a) => {
return (new Date(parseInt(a.replace('database/dbbackup-', '').replace('dbbackup-','')) * 100)).toLocaleString()
}))
]
const alertResult = parseInt(
await alertSelect(selectOptions)
) - 1
if(alertResult === -1){
return
}
const selectedBackup = internalBackups[alertResult]
const data = isTauri ? (
await readBinaryFile('database/' + selectedBackup, {dir: BaseDirectory.AppData})
) : (await forageStorage.getItem(selectedBackup))
setDatabase(
decodeRisuSave(data)
)
await alertNormal('Loaded backup')
}

View File

@@ -16,7 +16,7 @@ const magicCompressedHeader = new Uint8Array([0, 82, 73, 83, 85, 83, 65, 86, 69,
export function encodeRisuSave(data:any, compression:'noCompression'|'compression' = 'noCompression'){
let encoded:Uint8Array = packr.encode(data)
if(isTauri || compression === 'compression'){
if(compression === 'compression'){
encoded = fflate.compressSync(encoded)
const result = new Uint8Array(encoded.length + magicCompressedHeader.length);
result.set(magicCompressedHeader, 0)

View File

@@ -1,7 +1,9 @@
import { get, writable } from "svelte/store";
import { DataBase, type character, type groupChat } from "./storage/database";
import { get, writable, type Writable } from "svelte/store";
import { DataBase, type Chat, type character, type groupChat } from "./storage/database";
import { isEqual } from "lodash";
import type { simpleCharacterArgument } from "./parser";
import { getUserIcon, getUserIconProtrait, getUserName, sleep } from "./util";
import { getModules } from "./process/modules";
function updateSize(){
SizeStore.set({
@@ -23,25 +25,29 @@ export const CharEmotion = writable({} as {[key:string]: [string, string, number
export const ViewBoxsize = writable({ width: 12 * 16, height: 12 * 16 }); // Default width and height in pixels
export const settingsOpen = writable(false)
export const botMakerMode = writable(false)
export const moduleBackgroundEmbedding = writable('')
//optimization
let db = get(DataBase)
let currentChar = get(selectedCharID)
let currentCharacter = db.characters ? (db.characters[currentChar]) : null
let currentChat = currentCharacter ? (currentCharacter.chats[currentCharacter.chatPage]) : null
export const CurrentCharacter = writable(structuredClone(currentCharacter))
export const CurrentSimpleCharacter = writable(createSimpleCharacter(currentCharacter))
export const CurrentChat = writable(structuredClone(currentChat))
export const CurrentUsername = writable(db.username)
export const CurrentUserIcon = writable(db.userIcon)
export const CurrentShowMemoryLimit = writable(db.showMemoryLimit)
export const CurrentCharacter = writable(null) as Writable<character | groupChat>
export const CurrentSimpleCharacter = writable(null) as Writable<simpleCharacterArgument>
export const CurrentChat = writable(null) as Writable<Chat>
export const CurrentUsername = writable('') as Writable<string>
export const CurrentUserIcon = writable('') as Writable<string>
export const CurrentShowMemoryLimit = writable(false) as Writable<boolean>
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)
let lastGlobalEnabledModules: string[] = []
let lastChatEnabledModules: string[] = []
let moduleHideIcon = false
let characterHideIcon = false
function createSimpleCharacter(char:character|groupChat){
if((!char) || char.type === 'group'){
@@ -55,107 +61,196 @@ function createSimpleCharacter(char:character|groupChat){
additionalAssets: char.additionalAssets,
virtualscript: char.virtualscript,
emotionImages: char.emotionImages,
triggerscript: char.triggerscript,
}
return simpleChar
}
function updateCurrentCharacter(){
const db = get(DataBase)
if(!db.characters){
CurrentCharacter.set(null)
updateCurrentChat()
return
}
const currentCharId = get(selectedCharID)
const currentChar = db.characters[currentCharId]
const gotCharacter = get(CurrentCharacter)
if(isEqual(gotCharacter, currentChar)){
return
}
if((currentChar?.viewScreen === 'vn') !== get(ShowVN)){
ShowVN.set(currentChar?.viewScreen === 'vn')
}
CurrentCharacter.set(structuredClone(currentChar))
const simp = createSimpleCharacter(currentChar)
if(!isEqual(get(CurrentSimpleCharacter), simp)){
CurrentSimpleCharacter.set(simp)
}
updateCurrentChat()
function trySync(){
try {
let db = get(DataBase)
let currentChar = get(selectedCharID)
let currentCharacter = db.characters ? (db.characters[currentChar]) : null
let currentChat = currentCharacter ? (currentCharacter.chats[currentCharacter.chatPage]) : null
CurrentCharacter.set(structuredClone(currentCharacter))
CurrentSimpleCharacter.set(createSimpleCharacter(currentCharacter))
CurrentChat.set(structuredClone(currentChat))
CurrentUsername.set(getUserName())
CurrentUserIcon.set(getUserIcon())
CurrentShowMemoryLimit.set(db.showMemoryLimit)
} catch (error) {}
}
function updateCurrentChat(){
const currentChar = get(CurrentCharacter)
if(!currentChar){
CurrentChat.set(null)
return
}
const chat = (currentChar.chats[currentChar.chatPage])
const gotChat = get(CurrentChat)
if(isEqual(gotChat, chat)){
return
}
CurrentChat.set(structuredClone(chat))
}
trySync()
DataBase.subscribe((data) => {
updateCurrentCharacter()
if(data.username !== get(CurrentUsername)){
CurrentUsername.set(data.username)
}
if(data.userIcon !== get(CurrentUserIcon)){
CurrentUserIcon.set(data.userIcon)
}
if(data.showMemoryLimit !== get(CurrentShowMemoryLimit)){
CurrentShowMemoryLimit.set(data.showMemoryLimit)
}
})
selectedCharID.subscribe((id) => {
updateCurrentCharacter()
})
CurrentCharacter.subscribe((char) => {
updateCurrentChat()
let db = get(DataBase)
let charId = get(selectedCharID)
if(charId === -1 || charId > db.characters.length){
return
}
let cha = db.characters[charId]
if(isEqual(cha, char)){
return
}
db.characters[charId] = structuredClone(char)
DataBase.set(db)
})
CurrentChat.subscribe((chat) => {
let currentChar = get(CurrentCharacter)
if(currentChar){
if(!isEqual(currentChar.chats[currentChar.chatPage], chat)){
currentChar.chats[currentChar.chatPage] = structuredClone(chat)
CurrentCharacter.set(currentChar)
async function preInit(){
await sleep(1)
trySync()
function updateCurrentCharacter(){
const db = get(DataBase)
if(!db.characters){
CurrentCharacter.set(null)
updateCurrentChat()
return
}
const currentCharId = get(selectedCharID)
const currentChar = db.characters[currentCharId]
const gotCharacter = get(CurrentCharacter)
if(isEqual(gotCharacter, currentChar)){
return
}
if((currentChar?.viewScreen === 'vn') !== get(ShowVN)){
ShowVN.set(currentChar?.viewScreen === 'vn')
}
CurrentCharacter.set(structuredClone(currentChar))
const simp = createSimpleCharacter(currentChar)
if(!isEqual(get(CurrentSimpleCharacter), simp)){
CurrentSimpleCharacter.set(simp)
}
updateCurrentChat()
}
const variablePointer = get(CurrentVariablePointer)
const currentState = structuredClone(chat?.scriptstate)
if(!isEqual(variablePointer, currentState)){
CurrentVariablePointer.set(currentState)
function updateCurrentChat(){
const currentChar = get(CurrentCharacter)
if(!currentChar){
CurrentChat.set(null)
return
}
const chat = (currentChar.chats[currentChar.chatPage])
const gotChat = get(CurrentChat)
if(isEqual(gotChat, chat)){
return
}
CurrentChat.set(structuredClone(chat))
}
})
DataBase.subscribe((data) => {
updateCurrentCharacter()
if(getUserName() !== get(CurrentUsername)){
CurrentUsername.set(getUserName())
}
if(getUserIcon() !== get(CurrentUserIcon)){
CurrentUserIcon.set(getUserIcon())
}
if(getUserIconProtrait() !== get(UserIconProtrait)){
UserIconProtrait.set(getUserIconProtrait())
}
if(data.showMemoryLimit !== get(CurrentShowMemoryLimit)){
CurrentShowMemoryLimit.set(data.showMemoryLimit)
}
if(!isEqual(data.enabledModules, lastGlobalEnabledModules)){
lastGlobalEnabledModules = data.enabledModules || []
onModuleUpdate()
return
}
})
selectedCharID.subscribe((id) => {
updateCurrentCharacter()
})
CurrentCharacter.subscribe((char) => {
updateCurrentChat()
let db = get(DataBase)
let charId = get(selectedCharID)
if(char?.hideChatIcon !== characterHideIcon){
characterHideIcon = char?.hideChatIcon
HideIconStore.set(characterHideIcon || moduleHideIcon)
}
if(getUserName() !== get(CurrentUsername)){
CurrentUsername.set(getUserName())
}
if(getUserIcon() !== get(CurrentUserIcon)){
CurrentUserIcon.set(getUserIcon())
}
if(getUserIconProtrait() !== get(UserIconProtrait)){
UserIconProtrait.set(getUserIconProtrait())
}
if(charId === -1 || charId > db.characters.length){
return
}
let cha = db.characters[charId]
if(isEqual(cha, char)){
return
}
db.characters[charId] = structuredClone(char)
DataBase.set(db)
})
CurrentChat.subscribe((chat) => {
let currentChar = get(CurrentCharacter)
if(currentChar){
if(!isEqual(currentChar.chats[currentChar.chatPage], chat)){
currentChar.chats[currentChar.chatPage] = structuredClone(chat)
CurrentCharacter.set(currentChar)
}
}
if(!isEqual(lastChatEnabledModules, chat?.modules)){
lastChatEnabledModules = chat?.modules || []
onModuleUpdate()
return
}
if(getUserName() !== get(CurrentUsername)){
CurrentUsername.set(getUserName())
}
if(getUserIcon() !== get(CurrentUserIcon)){
CurrentUserIcon.set(getUserIcon())
}
const variablePointer = get(CurrentVariablePointer)
const currentState = structuredClone(chat?.scriptstate)
if(!isEqual(variablePointer, currentState)){
CurrentVariablePointer.set(currentState)
}
})
}
function onModuleUpdate(){
if(!Array.isArray(lastGlobalEnabledModules)){
lastGlobalEnabledModules = []
}
if(!Array.isArray(lastChatEnabledModules)){
lastChatEnabledModules = []
}
const m = getModules([
...lastGlobalEnabledModules, ...lastChatEnabledModules
])
let moduleHideIcon = false
let backgroundEmbedding = ''
m.forEach((module) => {
if(!module){
return
}
if(module.hideIcon){
moduleHideIcon = true
}
if(module.backgroundEmbedding){
backgroundEmbedding += '\n' + module.backgroundEmbedding + '\n'
}
})
if(backgroundEmbedding){
moduleBackgroundEmbedding.set(backgroundEmbedding)
}
HideIconStore.set(characterHideIcon || moduleHideIcon)
}
updateSize()
window.addEventListener("resize", updateSize);
window.addEventListener("resize", updateSize);
preInit()

View File

@@ -1,33 +0,0 @@
interface TextSyntSyntaxTree {
name: string;
children: TextSyntSyntaxTree[];
}
function parseMarkdownLikeYaml(text:string){
const lines = text.split('\n');
const root: TextSyntSyntaxTree = {name: 'root', children: []};
let rootHashIndentation = -1;
let currentIndentation = 0;
for(let i = 0; i < lines.length; i++){
const line = lines[i];
if(line.startsWith('#')){
let indentations = 0;
while(line[indentations] === '#'){
indentations++;
}
if(currentIndentation === 0 && rootHashIndentation === -1){
rootHashIndentation = indentations;
indentations = 1;
}
else{
indentations -= (rootHashIndentation - 1)
}
}
}
}

View File

@@ -17,6 +17,7 @@ export const tokenizerList = [
['llama', 'Llama'],
['llama3', 'Llama3'],
['novellist', 'Novellist'],
['gemma', 'Gemma'],
] as const
export async function encode(data:string):Promise<(number[]|Uint32Array|Int32Array)>{
@@ -35,6 +36,8 @@ export async function encode(data:string):Promise<(number[]|Uint32Array|Int32Arr
return await tokenizeWebTokenizers(data, 'novellist')
case 'llama3':
return await tokenizeWebTokenizers(data, 'llama')
case 'gemma':
return await tokenizeWebTokenizers(data, 'gemma')
default:
// Add exception for gpt-4o tokenizers on reverse_proxy
if(db.proxyRequestModel?.startsWith('gpt4o') ||

View File

@@ -4,11 +4,11 @@ import { DataBase, type character, type customscript, type groupChat } from "../
import { globalFetch, isTauri } from "../storage/globalApi"
import { alertError } from "../alert"
import { requestChatData } from "../process/request"
import { doingChat } from "../process"
import type { simpleCharacterArgument } from "../parser"
import { doingChat, type OpenAIChat } from "../process"
import { applyMarkdownToNode, parseChatML, type simpleCharacterArgument } from "../parser"
import { selectedCharID } from "../stores"
import { getModuleRegexScripts } from "../process/modules"
import { getNodetextToSentence, sleep, applyMarkdownToNode } from "../util"
import { getNodetextToSentence, sleep } from "../util"
import { processScriptFull } from "../process/scripts"
import { Capacitor } from "@capacitor/core"
@@ -448,11 +448,23 @@ async function translateLLM(text:string, arg:{to:string}){
if(llmCache.has(text)){
return llmCache.get(text)
}
const styleDecodeRegex = /\<risu-style\>(.+?)\<\/risu-style\>/gms
let styleDecodes:string[] = []
text = text.replace(styleDecodeRegex, (match, p1) => {
styleDecodes.push(p1)
return `<style-data style-index="${styleDecodes.length-1}"></style-data>`
})
const db = get(DataBase)
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.`
prompt = prompt.replace('{{slot}}', arg.to)
const rq = await requestChatData({
formated: [
let parsedPrompt = parseChatML(prompt.replaceAll('{{slot}}', arg.to).replaceAll('{{solt::content}}', text))
if(parsedPrompt){
formated = parsedPrompt
}
else{
prompt = prompt.replaceAll('{{slot}}', arg.to)
formated = [
{
'role': 'system',
'content': prompt
@@ -461,7 +473,10 @@ async function translateLLM(text:string, arg:{to:string}){
'role': 'user',
'content': text
}
],
]
}
const rq = await requestChatData({
formated,
bias: {},
useStreaming: false,
noMultiGen: true,
@@ -472,6 +487,9 @@ async function translateLLM(text:string, arg:{to:string}){
alertError(`${rq.result}`)
return text
}
llmCache.set(text, rq.result)
return rq.result
const result = rq.result.replace(/<style-data style-index="(\d+)" ?\/?>/g, (match, p1) => {
return styleDecodes[parseInt(p1)] ?? ''
}).replace(/<\/style-data>/g, '')
llmCache.set(text, result)
return result
}

View File

@@ -8,19 +8,9 @@ import { basename } from "@tauri-apps/api/path"
import { createBlankChar, getCharImage } from "./characters"
import { appWindow } from '@tauri-apps/api/window';
import { isTauri } from "./storage/globalApi"
import { Marked } from "marked"
export const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1
const mconverted = new Marked({
gfm: true,
breaks: true,
silent: true,
tokenizer: {
}
})
export interface Messagec extends Message{
index: number
}
@@ -113,10 +103,69 @@ export const replacePlaceholders = (msg:string, name:string) => {
let selectedChar = get(selectedCharID)
let currentChar = db.characters[selectedChar]
return msg .replace(/({{char}})|({{Char}})|(<Char>)|(<char>)/gi, currentChar.name)
.replace(/({{user}})|({{User}})|(<User>)|(<user>)/gi, db.username)
.replace(/({{user}})|({{User}})|(<User>)|(<user>)/gi, getUserName())
.replace(/(\{\{((set)|(get))var::.+?\}\})/gu,'')
}
function checkPersonaBinded(){
try {
let db = get(DataBase)
const selectedChar = get(selectedCharID)
const character = db.characters[selectedChar]
const chat = character.chats[character.chatPage]
console.log(chat.bindedPersona)
if(!chat.bindedPersona){
return null
}
const persona = db.personas.find(v => v.id === chat.bindedPersona)
console.log(db.personas, persona)
return persona
} catch (error) {
return null
}
}
export function getUserName(){
const bindedPersona = checkPersonaBinded()
if(bindedPersona){
return bindedPersona.name
}
const db = get(DataBase)
return db.username ?? 'User'
}
export function getUserIcon(){
const bindedPersona = checkPersonaBinded()
console.log(`Icon: ${bindedPersona?.icon}`)
if(bindedPersona){
return bindedPersona.icon
}
const db = get(DataBase)
return db.userIcon ?? ''
}
export function getPersonaPrompt(){
const bindedPersona = checkPersonaBinded()
if(bindedPersona){
return bindedPersona.personaPrompt
}
const db = get(DataBase)
return db.personaPrompt ?? ''
}
export function getUserIconProtrait(){
try {
const bindedPersona = checkPersonaBinded()
if(bindedPersona){
return bindedPersona.largePortrait
}
const db = get(DataBase)
return db.personas[db.selectedPersona].largePortrait
} catch (error) {
return false
}
}
export function checkIsIos(){
return /(iPad|iPhone|iPod)/g.test(navigator.userAgent)
}
@@ -585,33 +634,6 @@ export function getNodetextToSentence(node: Node): string {
return result;
}
export function applyMarkdownToNode(node: Node) {
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent;
if (text) {
let markdown = mconverted.parseInline(text);
if (markdown !== text) {
const span = document.createElement('span');
span.innerHTML = markdown;
// inherit inline style from the parent node
const parentStyle = (node.parentNode as HTMLElement)?.style;
if(parentStyle){
for(let i=0;i<parentStyle.length;i++){
span.style.setProperty(parentStyle[i], parentStyle.getPropertyValue(parentStyle[i]))
}
}
(node as Element)?.replaceWith(span);
return
}
}
} else {
for (const child of node.childNodes) {
applyMarkdownToNode(child);
}
}
}
export const TagList = [
{
@@ -973,20 +995,24 @@ export const isKnownUri = (uri:string) => {
}
export function parseKeyValue(template:string){
if(!template){
try {
if(!template){
return []
}
const keyValue:[string, string][] = []
for(const line of template.split('\n')){
const [key, value] = line.split('=')
if(key && value){
keyValue.push([key, value])
}
}
return keyValue
} catch (error) {
return []
}
const keyValue:[string, string][] = []
for(const line of template.split('\n')){
const [key, value] = line.split('=')
if(key && value){
keyValue.push([key, value])
}
}
return keyValue
}
export const sortableOptions = {