Problem: The Node.js hosted version of RisuAI encountered an issue where it failed to fetch data from the Risu Realm server when accessed remotely. RisuAI's frontend directly fetches data from the Realm server (e.g., sv.risuai.xyz). While the official web version did not exhibit CORS errors (potentially due to same-origin deployment or specific server-side CORS configurations), running the Node.js version on a self-hosted server and accessing it remotely resulted in browser CORS policy violations. Solution: The fix involves detecting when the frontend runs in the Node.js host environment. When this environment is detected, instead of requesting Realm data directly from the external server (sv.risuai.xyz), the frontend now directs the request to a new proxy endpoint (`/hub-proxy/*`) on its own backend server. The backend proxy then fetches the required data (including JSON and images) from the actual Realm server, correctly handles content types and compression, and relays the response back to the frontend. This ensures that, from the browser's perspective, the frontend is communicating with its same-origin backend, effectively bypassing browser CORS restrictions and resolving the data fetching issue.
1918 lines
68 KiB
TypeScript
1918 lines
68 KiB
TypeScript
import { get, writable, type Writable } from "svelte/store"
|
|
import { alertCardExport, alertConfirm, alertError, alertInput, alertMd, alertNormal, alertSelect, alertStore, alertTOS, alertWait } from "./alert"
|
|
import { defaultSdDataFunc, type character, setDatabase, type customscript, type loreSettings, type loreBook, type triggerscript, importPreset, type groupChat, setCurrentCharacter, getCurrentCharacter, getDatabase, setDatabaseLite, appVer } from "./storage/database.svelte"
|
|
import { checkNullish, decryptBuffer, encryptBuffer, isKnownUri, selectFileByDom, selectMultipleFile, sleep } from "./util"
|
|
import { language } from "src/lang"
|
|
import { v4 as uuidv4, v4 } from 'uuid';
|
|
import { characterFormatUpdate } from "./characters"
|
|
import { AppendableBuffer, BlankWriter, checkCharOrder, downloadFile, isNodeServer, isTauri, loadAsset, LocalWriter, openURL, readImage, saveAsset, VirtualWriter } from "./globalApi.svelte"
|
|
import { SettingsMenuIndex, ShowRealmFrameStore, selectedCharID, settingsOpen } from "./stores.svelte"
|
|
import { checkImageType, convertImage, hasher } from "./parser.svelte"
|
|
import { CCardLib, type CharacterCardV3, type LorebookEntry } from '@risuai/ccardlib'
|
|
import { reencodeImage } from "./process/files/inlays"
|
|
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';
|
|
|
|
|
|
const EXTERNAL_HUB_URL = 'https://sv.risuai.xyz';
|
|
export const hubURL = typeof window !== 'undefined' && (window as any).__NODE__ === true
|
|
? '/hub-proxy'
|
|
: EXTERNAL_HUB_URL;
|
|
|
|
export async function importCharacter() {
|
|
try {
|
|
const files = await selectFileByDom(["*"], 'multiple')
|
|
if(!files){
|
|
return
|
|
}
|
|
|
|
for(const f of files){
|
|
console.log(f)
|
|
await importCharacterProcess({
|
|
name: f.name,
|
|
data: f
|
|
})
|
|
checkCharOrder()
|
|
}
|
|
} catch (error) {
|
|
alertError(`${error}`)
|
|
return null
|
|
}
|
|
}
|
|
|
|
export async function importCharacterProcess(f:{
|
|
name: string;
|
|
data: Uint8Array|File|ReadableStream<Uint8Array>
|
|
lightningRealmImport?:boolean
|
|
}) {
|
|
if(f.name.endsWith('json')){
|
|
if(f.data instanceof ReadableStream){
|
|
return null
|
|
}
|
|
const data = f.data instanceof Uint8Array ? f.data : new Uint8Array(await f.data.arrayBuffer())
|
|
const da = JSON.parse(Buffer.from(data).toString('utf-8'))
|
|
if(await importCharacterCardSpec(da)){
|
|
let db = getDatabase()
|
|
return db.characters.length - 1
|
|
}
|
|
if((da.char_name || da.name) && (da.char_persona || da.description) && (da.char_greeting || da.first_mes)){
|
|
let db = getDatabase()
|
|
db.characters.push(convertOffSpecCards(da))
|
|
setDatabaseLite(db)
|
|
alertNormal(language.importedCharacter)
|
|
return
|
|
}
|
|
else{
|
|
alertError(language.errors.noData)
|
|
return
|
|
}
|
|
}
|
|
|
|
let db = getDatabase()
|
|
db.statics.imports += 1
|
|
|
|
if(f.name.endsWith('charx') || f.name.endsWith('jpg') || f.name.endsWith('jpeg')){
|
|
console.log('reading charx')
|
|
alertStore.set({
|
|
type: 'wait',
|
|
msg: 'Loading... (Reading)'
|
|
})
|
|
const reader = new CharXReader()
|
|
await reader.read(f.data, {
|
|
alertInfo: true
|
|
})
|
|
const cardData = reader.cardData
|
|
if(!cardData){
|
|
alertError(language.errors.noData)
|
|
return
|
|
}
|
|
const card:CharacterCardV3 = JSON.parse(cardData)
|
|
if(card.spec !== 'chara_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 = getDatabase()
|
|
return db.characters.length - 1
|
|
}
|
|
|
|
if(!f.name.endsWith('png')){
|
|
alertError(language.errors.noData)
|
|
return
|
|
}
|
|
|
|
|
|
alertStore.set({
|
|
type: 'wait',
|
|
msg: 'Loading... (Reading)'
|
|
})
|
|
await sleep(10)
|
|
|
|
// const readed = PngChunk.read(img, ['chara'])?.['chara']
|
|
let readedChara = ''
|
|
let readedCCv3 = ''
|
|
let img:Uint8Array
|
|
let pngChunks = 0
|
|
let readedPngChunks = 0
|
|
|
|
{
|
|
|
|
let readData:File | Uint8Array | ReadableStream<Uint8Array>
|
|
if(f.data instanceof ReadableStream){
|
|
const tee = f.data.tee()
|
|
f.data = tee[0]
|
|
readData = tee[1]
|
|
}
|
|
else{
|
|
readData = f.data
|
|
}
|
|
|
|
const prereader = PngChunk.readGenerator(readData, {
|
|
|
|
})
|
|
|
|
for await(const chunk of prereader){
|
|
if(chunk instanceof AppendableBuffer){
|
|
break
|
|
}
|
|
if(chunk.key.startsWith('chara-ext-asset_')){
|
|
pngChunks++
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
const readGenerator = PngChunk.readGenerator(f.data, {
|
|
returnTrimed: true
|
|
})
|
|
const assets:{[key:string]:string} = {}
|
|
let queueFetch:Promise<Response>[] = []
|
|
let queueFetchKey:string[] = []
|
|
let queueFetchData:Buffer[] = []
|
|
for await (const chunk of readGenerator){
|
|
console.log(chunk)
|
|
if(!chunk){
|
|
continue
|
|
}
|
|
if(chunk instanceof AppendableBuffer){
|
|
img = chunk.buffer
|
|
break
|
|
}
|
|
if(chunk.key === 'chara'){
|
|
//For memory reason, limit to 5MB
|
|
if(readedChara.length < 5 * 1024 * 1024){
|
|
readedChara = chunk.value
|
|
}
|
|
continue
|
|
}
|
|
if(chunk.key === 'ccv3'){
|
|
if(readedCCv3.length < 5 * 1024 * 1024){
|
|
readedCCv3 = chunk.value
|
|
}
|
|
continue
|
|
}
|
|
if(chunk.key.startsWith('chara-ext-asset_')){
|
|
const assetIndex = chunk.key.replace('chara-ext-asset_:', '').replace('chara-ext-asset_', '')
|
|
const assetData = Buffer.from(chunk.value, 'base64')
|
|
if(pngChunks === 0){
|
|
alertWait('Loading... (Loaded ' + readedPngChunks + ' Assets)')
|
|
}
|
|
else{
|
|
alertStore.set({
|
|
type: 'progress',
|
|
msg: 'Loading... (Loading Assets)',
|
|
submsg: (readedPngChunks / pngChunks * 100).toFixed(2)
|
|
})
|
|
}
|
|
|
|
readedPngChunks++
|
|
|
|
if(db.account?.useSync && f.lightningRealmImport){
|
|
const id = await hasher(assetData)
|
|
const xid = 'assets/' + id + '.png'
|
|
queueFetchKey.push(assetIndex)
|
|
queueFetchData.push(assetData)
|
|
queueFetch.push(fetch('https://sv.risuai.xyz/rs/' + xid))
|
|
assets[assetIndex] = 'xid:' + xid
|
|
if(queueFetch.length > 10){
|
|
const res = await Promise.all(queueFetch)
|
|
for(let i=0;i<res.length;i++){
|
|
if(res[i].status !== 200){
|
|
const assetId = await saveAsset(queueFetchData[i])
|
|
assets[queueFetchKey[i]] = assetId
|
|
}
|
|
else{
|
|
assets[queueFetchKey[i]] = assets[queueFetchKey[i]].replace('xid:', '')
|
|
}
|
|
}
|
|
queueFetch = []
|
|
queueFetchKey = []
|
|
queueFetchData = []
|
|
}
|
|
continue
|
|
}
|
|
|
|
|
|
const assetId = await saveAsset(assetData)
|
|
assets[assetIndex] = assetId
|
|
}
|
|
}
|
|
|
|
if(queueFetch.length > 0){
|
|
const res = await Promise.all(queueFetch)
|
|
for(let i=0;i<res.length;i++){
|
|
if(res[i].status !== 200){
|
|
const assetId = await saveAsset(queueFetchData[i])
|
|
assets[queueFetchKey[i]] = assetId
|
|
}
|
|
else{
|
|
assets[queueFetchKey[i]] = assets[queueFetchKey[i]].replace('xid:', '')
|
|
}
|
|
}
|
|
queueFetch = []
|
|
queueFetchKey = []
|
|
queueFetchData = []
|
|
}
|
|
|
|
if(!readedChara && !readedCCv3){
|
|
alertError(language.errors.noData)
|
|
return
|
|
}
|
|
|
|
if(readedCCv3){
|
|
readedChara = readedCCv3
|
|
}
|
|
|
|
if(!img){
|
|
console.error("No Image Found")
|
|
alertError(language.errors.noData)
|
|
return
|
|
}
|
|
|
|
if(readedChara.startsWith('rcc||')){
|
|
const parts = readedChara.split('||')
|
|
const type = parts[1]
|
|
if(type === 'rccv1'){
|
|
if(parts.length !== 5){
|
|
alertError(language.errors.noData)
|
|
return
|
|
}
|
|
const encrypted = Buffer.from(parts[2], 'base64')
|
|
const hashed = await hasher(encrypted)
|
|
if(hashed !== parts[3]){
|
|
alertError(language.errors.noData)
|
|
return
|
|
}
|
|
const metaData:RccCardMetaData = JSON.parse(Buffer.from(parts[4], 'base64').toString('utf-8'))
|
|
if(metaData.usePassword){
|
|
const password = await alertInput(language.inputCardPassword)
|
|
if(!password){
|
|
return
|
|
}
|
|
else{
|
|
try {
|
|
const decrypted = await decryptBuffer(encrypted, password)
|
|
const charaData:CharacterCardV2Risu = JSON.parse(Buffer.from(decrypted).toString('utf-8'))
|
|
if(await importCharacterCardSpec(charaData, img, "normal", assets)){
|
|
let db = getDatabase()
|
|
return db.characters.length - 1
|
|
}
|
|
else{
|
|
throw new Error('Error while importing')
|
|
}
|
|
} catch (error) {
|
|
alertError(language.errors.wrongPassword)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
else{
|
|
const decrypted = await decryptBuffer(encrypted, 'RISU_NONE')
|
|
try {
|
|
const charaData:CharacterCardV2Risu = JSON.parse(Buffer.from(decrypted).toString('utf-8'))
|
|
if(await importCharacterCardSpec(charaData, img, "normal", assets)){
|
|
let db = getDatabase()
|
|
return db.characters.length - 1
|
|
}
|
|
} catch (error) {
|
|
alertError(language.errors.noData)
|
|
return
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
const parsed = JSON.parse(Buffer.from(readedChara, 'base64').toString('utf-8'))
|
|
//fix readedChara version pointing number instead of string because of previous version
|
|
if(typeof (parsed as CharacterCardV2Risu)?.data?.character_version === 'number'){
|
|
(parsed as CharacterCardV2Risu).data.character_version = (parsed as CharacterCardV2Risu).data.character_version.toString()
|
|
}
|
|
|
|
if(parsed.spec !== 'chara_card_v2' && parsed.spec !== 'chara_card_v3'){
|
|
const charaData:OldTavernChar = JSON.parse(Buffer.from(readedChara, 'base64').toString('utf-8'))
|
|
console.log(charaData)
|
|
const imgp = await saveAsset(img)
|
|
db.characters.push(convertOffSpecCards(charaData, imgp))
|
|
setDatabaseLite(db)
|
|
alertNormal(language.importedCharacter)
|
|
return db.characters.length - 1
|
|
}
|
|
await importCharacterCardSpec(parsed, img, "normal", assets)
|
|
|
|
db = getDatabase()
|
|
return db.characters.length - 1
|
|
|
|
}
|
|
|
|
export const getRealmInfo = async (realmPath:string) => {
|
|
const url = new URL(location.href);
|
|
url.searchParams.delete('realm');
|
|
window.history.pushState(null, '', url.toString());
|
|
|
|
const res = await fetch(`${hubURL}/hub/info/${realmPath}`)
|
|
if(res.status !== 200){
|
|
alertError(await res.text())
|
|
return
|
|
}
|
|
showRealmInfoStore.set(await res.json())
|
|
}
|
|
|
|
export const showRealmInfoStore:Writable<null|hubType> = writable(null)
|
|
|
|
export async function characterURLImport() {
|
|
const realmPath = (new URLSearchParams(location.search)).get('realm')
|
|
try {
|
|
if(realmPath){
|
|
getRealmInfo(realmPath)
|
|
}
|
|
} catch (error) {
|
|
|
|
}
|
|
|
|
const charPath = (new URLSearchParams(location.search)).get('charahub')
|
|
try {
|
|
if(charPath){
|
|
alertWait('Loading from Chub...')
|
|
const url = new URL(location.href);
|
|
url.searchParams.delete('charahub');
|
|
window.history.pushState(null, '', url.toString());
|
|
const chara = await fetch("https://api.chub.ai/api/characters/download", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
"format": "tavern",
|
|
"fullPath": charPath,
|
|
"version": "main"
|
|
}),
|
|
headers: {
|
|
"content-type": "application/json"
|
|
}
|
|
})
|
|
const img = new Uint8Array(await chara.arrayBuffer())
|
|
await importCharacterProcess({
|
|
name: 'charahub.png',
|
|
data: img
|
|
})
|
|
}
|
|
} catch (error) {
|
|
alertError(language.errors.noData)
|
|
return null
|
|
}
|
|
|
|
|
|
const hash = location.hash
|
|
if(hash.startsWith('#import=')){
|
|
location.hash = ''
|
|
const url = hash.replace('#import=', '')
|
|
try {
|
|
const res = await fetch(url, {
|
|
method: 'GET',
|
|
})
|
|
const data = new Uint8Array(await res.arrayBuffer())
|
|
await importFile(getFileName(res), data)
|
|
checkCharOrder()
|
|
} catch (error) {
|
|
alertError(language.errors.noData)
|
|
return null
|
|
}
|
|
}
|
|
if(hash.startsWith('#import_module=')){
|
|
const data = hash.replace('#import_module=', '')
|
|
const importData = JSON.parse(Buffer.from(decodeURIComponent(data), 'base64').toString('utf-8'))
|
|
importData.id = v4()
|
|
|
|
const db = getDatabase()
|
|
if(importData.lowLevelAccess){
|
|
const conf = await alertConfirm(language.lowLevelAccessConfirm)
|
|
if(!conf){
|
|
return false
|
|
}
|
|
}
|
|
db.modules.push(importData)
|
|
setDatabase(db)
|
|
alertNormal(language.successImport)
|
|
SettingsMenuIndex.set(14)
|
|
settingsOpen.set(true)
|
|
return
|
|
}
|
|
if(hash.startsWith('#import_preset=')){
|
|
const data = hash.replace('#import_preset=', '')
|
|
const importData =Buffer.from(decodeURIComponent(data), 'base64')
|
|
await importPreset({
|
|
name: 'imported.risupreset',
|
|
data: importData
|
|
})
|
|
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 = getDatabase()
|
|
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())
|
|
await 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)
|
|
await 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('.jpg') || name.endsWith('.jpeg') || 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 = getDatabase()
|
|
db.modules.push(md)
|
|
setDatabase(db)
|
|
alertNormal(language.successImport)
|
|
SettingsMenuIndex.set(14)
|
|
settingsOpen.set(true)
|
|
return
|
|
}
|
|
}
|
|
|
|
function getFileName(res : Response) : string {
|
|
return getFromContent(res.headers.get('content-disposition')) || getFromURL(res.url);
|
|
|
|
function getFromContent(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 "";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
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){
|
|
const a = convertCharbook({
|
|
lorebook,
|
|
charbook,
|
|
loresettings,
|
|
loreExt
|
|
})
|
|
|
|
lorebook = a.lorebook
|
|
loresettings = a.loresettings
|
|
loreExt = a.loreExt
|
|
}
|
|
|
|
return {
|
|
name: data.name ?? 'unknown name',
|
|
firstMessage: data.first_mes ?? 'unknown first message',
|
|
desc: data.description ?? '',
|
|
notes: '',
|
|
chats: [{
|
|
message: [],
|
|
note: '',
|
|
name: 'Chat 1',
|
|
localLore: []
|
|
}],
|
|
chatPage: 0,
|
|
image: imgp,
|
|
emotionImages: [],
|
|
bias: [],
|
|
globalLore: lorebook,
|
|
viewScreen: 'none',
|
|
chaId: uuidv4(),
|
|
sdData: defaultSdDataFunc(),
|
|
utilityBot: false,
|
|
customscript: [],
|
|
exampleMessage: data.mes_example,
|
|
creatorNotes:'',
|
|
systemPrompt: (charaData.spec_version === '2.0' ? charaData.data.system_prompt : '') ?? '',
|
|
postHistoryInstructions: (charaData.spec_version === '2.0' ? charaData.data.post_history_instructions : '') ?? '',
|
|
alternateGreetings:[],
|
|
tags:[],
|
|
creator:"",
|
|
characterVersion: '',
|
|
personality: data.personality ?? '',
|
|
scenario:data.scenario ?? '',
|
|
firstMsgIndex: -1,
|
|
replaceGlobalNote: "",
|
|
triggerscript: [],
|
|
additionalText: '',
|
|
loreExt: loreExt,
|
|
loreSettings: loresettings,
|
|
|
|
}
|
|
}
|
|
|
|
export async function exportChar(charaID:number):Promise<string> {
|
|
const db = getDatabase({snapshot: true})
|
|
let char = safeStructuredClone(db.characters[charaID])
|
|
|
|
if(char.type === 'group'){
|
|
return ''
|
|
}
|
|
|
|
if(!char.image){
|
|
alertError('Image Required')
|
|
return ''
|
|
}
|
|
|
|
const option = await alertCardExport()
|
|
if(option.type === ''){
|
|
exportCharacterCard(char, option.type2 === 'json' ? 'json' : (option.type2 === 'charx' ? 'charx' : option.type2 === 'charxJpeg' ? 'charxJpeg' : 'png'), {spec: 'v3'})
|
|
}
|
|
else if(option.type === 'ccv2'){
|
|
exportCharacterCard(char,'png', {spec: 'v2'})
|
|
}
|
|
else if(option.type === 'realm'){
|
|
ShowRealmFrameStore.set("character")
|
|
}
|
|
else{
|
|
return option.type
|
|
}
|
|
return ''
|
|
}
|
|
|
|
|
|
async function importCharacterCardSpec(card:CharacterCardV2Risu|CharacterCardV3, img?:Uint8Array, mode:'hub'|'normal' = 'normal', assetDict:{[key:string]:string} = {}):Promise<boolean>{
|
|
if(!card ||(card.spec !== 'chara_card_v2' && card.spec !== 'chara_card_v3' )){
|
|
return false
|
|
}
|
|
|
|
console.log(`Importing ${card.spec}, mode is ${mode}`)
|
|
|
|
const data = card.data
|
|
console.log(card)
|
|
let im = img ? await saveAsset(img) : undefined
|
|
let db = getDatabase()
|
|
|
|
const risuext = safeStructuredClone(data.extensions.risuai)
|
|
let emotions:[string, string][] = []
|
|
let bias:[string, number][] = []
|
|
let viewScreen: "none" | "emotion" | "imggen" = 'none'
|
|
let customScripts:customscript[] = []
|
|
let utilityBot = false
|
|
let sdData = defaultSdDataFunc()
|
|
let extAssets:[string,string,string][] = []
|
|
let ccAssets:{
|
|
type: string
|
|
uri: string
|
|
name: string
|
|
ext: string
|
|
}[] = []
|
|
|
|
let vits:null|OnnxModelFiles = null
|
|
if(risuext && card.spec === 'chara_card_v2'){
|
|
if(risuext.emotions){
|
|
for(let i=0;i<risuext.emotions.length;i++){
|
|
alertStore.set({
|
|
type: 'progress',
|
|
msg: `Loading... (Loading Emotions)`,
|
|
submsg: (i / risuext.emotions.length * 100).toFixed(2)
|
|
})
|
|
await sleep(10)
|
|
if(risuext.emotions[i][1].startsWith('__asset:')){
|
|
const key = risuext.emotions[i][1].replace('__asset:', '')
|
|
const imgp = assetDict[key]
|
|
if(!imgp){
|
|
throw new Error('Error while importing, asset ' + key + ' not found')
|
|
}
|
|
emotions.push([risuext.emotions[i][0],imgp])
|
|
continue
|
|
}
|
|
const imgp = await saveAsset(mode === 'hub' ? (await getHubResources(risuext.emotions[i][1])) : Buffer.from(risuext.emotions[i][1], 'base64'))
|
|
emotions.push([risuext.emotions[i][0],imgp])
|
|
}
|
|
}
|
|
if(risuext.additionalAssets){
|
|
for(let i=0;i<risuext.additionalAssets.length;i++){
|
|
alertStore.set({
|
|
type: 'progress',
|
|
msg: `Loading... (Loading Assets)`,
|
|
submsg: (i / risuext.additionalAssets.length * 100).toFixed(2)
|
|
})
|
|
await sleep(10)
|
|
let fileName = ''
|
|
if(risuext.additionalAssets[i].length >= 3)
|
|
fileName = risuext.additionalAssets[i][2]
|
|
if(risuext.additionalAssets[i][1].startsWith('__asset:')){
|
|
const key = risuext.additionalAssets[i][1].replace('__asset:', '')
|
|
const imgp = assetDict[key]
|
|
if(!imgp){
|
|
throw new Error('Error while importing, asset ' + key + ' not found')
|
|
}
|
|
extAssets.push([risuext.additionalAssets[i][0],imgp,fileName])
|
|
continue
|
|
}
|
|
const imgp = await saveAsset(mode === 'hub' ? (await getHubResources(risuext.additionalAssets[i][1])) :Buffer.from(risuext.additionalAssets[i][1], 'base64'), '', fileName)
|
|
extAssets.push([risuext.additionalAssets[i][0],imgp,fileName])
|
|
}
|
|
}
|
|
if(risuext.vits){
|
|
const keys = Object.keys(risuext.vits)
|
|
for(let i=0;i<keys.length;i++){
|
|
alertStore.set({
|
|
type: 'progress',
|
|
msg: `Loading... (Loading VITS)`,
|
|
submsg: (i / keys.length * 100).toFixed(2)
|
|
})
|
|
await sleep(10)
|
|
const key = keys[i]
|
|
if(risuext.vits[key].startsWith('__asset:')){
|
|
const rkey = risuext.vits[key].replace('__asset:', '')
|
|
const imgp = assetDict[rkey]
|
|
if(!imgp){
|
|
throw new Error('Error while importing, asset ' + rkey + ' not found')
|
|
}
|
|
risuext.vits[key] = imgp
|
|
continue
|
|
}
|
|
const imgp = await saveAsset(mode === 'hub' ? (await getHubResources(risuext.vits[key])) : Buffer.from(risuext.vits[key], 'base64'))
|
|
risuext.vits[key] = imgp
|
|
}
|
|
|
|
if(keys.length > 0){
|
|
vits = {
|
|
name: "Imported VITS",
|
|
files: risuext.vits,
|
|
id: uuidv4().replace(/-/g, '')
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
if(risuext){
|
|
bias = risuext.bias ?? bias
|
|
viewScreen = risuext.viewScreen ?? viewScreen
|
|
customScripts = risuext.customScripts ?? customScripts
|
|
utilityBot = risuext.utilityBot ?? utilityBot
|
|
sdData = risuext.sdData ?? sdData
|
|
}
|
|
}
|
|
if(card.spec === 'chara_card_v3'){
|
|
const data = card.data //required for type checking
|
|
if(data.assets){
|
|
for(let i=0;i<data.assets.length;i++){
|
|
alertStore.set({
|
|
type: 'progress',
|
|
msg: `Loading... (Assets)`,
|
|
submsg: (i / data.assets.length * 100).toFixed(2)
|
|
})
|
|
await sleep(10)
|
|
let fileName = ''
|
|
let imgp = ''
|
|
if(data.assets[i].name){
|
|
fileName = data.assets[i].name
|
|
}
|
|
if(data.assets[i].uri.startsWith('__asset:')){
|
|
const key = data.assets[i].uri.replace('__asset:', '')
|
|
imgp = assetDict[key]
|
|
if(!imgp){
|
|
throw new Error('Error while importing, asset ' + key + ' not found')
|
|
}
|
|
}
|
|
else if(data.assets[i].uri === 'ccdefault:'){
|
|
imgp = im
|
|
}
|
|
else if(data.assets[i].uri.startsWith('embeded://')){
|
|
const key = data.assets[i].uri.replace('embeded://', '')
|
|
imgp = assetDict[key]
|
|
if(!imgp){
|
|
throw new Error('Error while importing, asset ' + key + ' not found')
|
|
}
|
|
}
|
|
else if(data.assets[i].uri.startsWith('data:')){
|
|
//data uri
|
|
const b64 = data.assets[i].uri.split(',')[1]
|
|
if(b64.length < 50 * 1024 * 1024){
|
|
imgp = await saveAsset(Buffer.from(b64, 'base64'))
|
|
}
|
|
else{
|
|
alertError('Data URI too large')
|
|
continue
|
|
}
|
|
}
|
|
else{
|
|
continue
|
|
}
|
|
if(data.assets[i].type === 'emotion'){
|
|
emotions.push([fileName,imgp])
|
|
}
|
|
else if(data.assets[i].type === 'x-risu-asset'){
|
|
extAssets.push([fileName,imgp, data.assets[i].ext ?? 'unknown'])
|
|
}
|
|
else if(data.assets[i].type === 'icon' && data.assets[i].name === 'main'){
|
|
im = imgp
|
|
}
|
|
else{
|
|
ccAssets.push({
|
|
type: data.assets[i].type ?? 'asset',
|
|
uri: imgp,
|
|
name: fileName,
|
|
ext: data.assets[i].ext ?? 'unknown'
|
|
})
|
|
console.log(ccAssets)
|
|
}
|
|
}
|
|
}
|
|
|
|
if(risuext){
|
|
bias = risuext.bias ?? bias
|
|
viewScreen = risuext.viewScreen ?? viewScreen
|
|
customScripts = risuext.customScripts ?? customScripts
|
|
utilityBot = risuext.utilityBot ?? utilityBot
|
|
sdData = risuext.sdData ?? sdData
|
|
}
|
|
}
|
|
|
|
if(risuext && risuext?.lowLevelAccess){
|
|
const conf = await alertConfirm(language.lowLevelAccessConfirm)
|
|
if(!conf){
|
|
return false
|
|
}
|
|
}
|
|
const charbook = data.character_book
|
|
let lorebook:loreBook[] = []
|
|
let loresettings:undefined|loreSettings = undefined
|
|
let loreExt:undefined|any = undefined
|
|
if(charbook){
|
|
const a = convertCharbook({
|
|
lorebook,
|
|
charbook,
|
|
loresettings,
|
|
loreExt
|
|
})
|
|
|
|
lorebook = a.lorebook
|
|
loresettings = a.loresettings
|
|
loreExt = a.loreExt
|
|
}
|
|
|
|
let ext = safeStructuredClone(data?.extensions ?? {})
|
|
|
|
for(const key in ext){
|
|
if(key === 'risuai'){
|
|
delete ext[key]
|
|
}
|
|
if(key === 'depth_prompt'){
|
|
delete ext[key]
|
|
}
|
|
}
|
|
|
|
let char:character = {
|
|
name: data.name ?? '',
|
|
firstMessage: data.first_mes ?? '',
|
|
desc: data.description ?? '',
|
|
notes: '',
|
|
chats: [{
|
|
message: [],
|
|
note: '',
|
|
name: 'Chat 1',
|
|
localLore: []
|
|
}],
|
|
chatPage: 0,
|
|
image: im,
|
|
emotionImages: emotions,
|
|
bias: bias,
|
|
globalLore: lorebook, //lorebook
|
|
viewScreen: viewScreen,
|
|
chaId: uuidv4(),
|
|
sdData: sdData,
|
|
utilityBot: utilityBot,
|
|
customscript: customScripts,
|
|
exampleMessage: data.mes_example ?? '',
|
|
creatorNotes:data.creator_notes ?? '',
|
|
systemPrompt:data.system_prompt ?? '',
|
|
postHistoryInstructions:'',
|
|
alternateGreetings:data.alternate_greetings ?? [],
|
|
tags:data.tags ?? [],
|
|
creator:data.creator ?? '',
|
|
characterVersion: `${data.character_version}` || '',
|
|
personality:data.personality ?? '',
|
|
scenario:data.scenario ?? '',
|
|
firstMsgIndex: -1,
|
|
removedQuotes: false,
|
|
loreSettings: loresettings,
|
|
loreExt: loreExt,
|
|
additionalData: {
|
|
tag: data.tags ?? [],
|
|
creator: data.creator,
|
|
character_version: data.character_version
|
|
},
|
|
additionalAssets: extAssets,
|
|
replaceGlobalNote: data.post_history_instructions ?? '',
|
|
backgroundHTML: data?.extensions?.risuai?.backgroundHTML,
|
|
license: data?.extensions?.risuai?.license,
|
|
triggerscript: data?.extensions?.risuai?.triggerscript ?? [],
|
|
private: data?.extensions?.risuai?.private ?? false,
|
|
additionalText: data?.extensions?.risuai?.additionalText ?? '',
|
|
virtualscript: '', //removed dude to security issue
|
|
extentions: ext ?? {},
|
|
largePortrait: data?.extensions?.risuai?.largePortrait ?? (!data?.extensions?.risuai),
|
|
lorePlus: data?.extensions?.risuai?.lorePlus ?? false,
|
|
inlayViewScreen: data?.extensions?.risuai?.inlayViewScreen ?? false,
|
|
newGenData: data?.extensions?.risuai?.newGenData ?? undefined,
|
|
vits: vits,
|
|
ttsMode: vits ? 'vits' : 'normal',
|
|
imported: true,
|
|
source: card?.data?.extensions?.risuai?.source ?? [],
|
|
ccAssets: ccAssets,
|
|
lowLevelAccess: risuext?.lowLevelAccess ?? false,
|
|
defaultVariables: data?.extensions?.risuai?.defaultVariables ?? '',
|
|
}
|
|
|
|
if(card.spec === 'chara_card_v3'){
|
|
char.group_only_greetings = card.data.group_only_greetings ?? []
|
|
char.nickname = card.data.nickname ?? ''
|
|
char.source = card.data.source ?? card.data?.extensions?.risuai?.source ?? []
|
|
char.creation_date = card.data.creation_date ?? 0
|
|
char.modification_date = card.data.modification_date ?? 0
|
|
}
|
|
|
|
db.characters.push(char)
|
|
|
|
|
|
setDatabase(db)
|
|
|
|
alertNormal(language.importedCharacter)
|
|
return true
|
|
|
|
}
|
|
|
|
function convertCharbook(arg:{
|
|
lorebook:loreBook[]
|
|
charbook:CharacterBook
|
|
loresettings:loreSettings
|
|
loreExt:any
|
|
}){
|
|
let {lorebook, loresettings, loreExt, charbook} = arg
|
|
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){
|
|
let content = book.content
|
|
|
|
if(book.use_regex && !book.keys?.[0]?.startsWith('/')){
|
|
book.use_regex = false
|
|
}
|
|
|
|
//extention migration
|
|
const extensions = book.extensions ?? {}
|
|
|
|
if(extensions.useProbability && extensions.probability !== undefined && extensions.probability !== 100){
|
|
content = `@@probability ${extensions.probability}\n` + content
|
|
delete extensions.useProbability
|
|
delete extensions.probability
|
|
}
|
|
if(extensions.position === 4 && typeof extensions.depth === 'number' && typeof(extensions.role) === 'number'){
|
|
content = `@@depth ${extensions.depth}\n@@role ${['system','user','assistant'][extensions.role]}\n` + content
|
|
delete extensions.position
|
|
delete extensions.depth
|
|
delete extensions.role
|
|
}
|
|
if(typeof(extensions.selectiveLogic) === 'number' && book.secondary_keys && book.secondary_keys.length > 0){
|
|
switch(extensions.selectiveLogic){
|
|
case 0:{
|
|
if(!book.secondary_keys || book.secondary_keys.length === 0){
|
|
book.selective = false
|
|
}
|
|
break
|
|
}
|
|
case 1:{
|
|
book.selective = false
|
|
content = `@@exclude_keys_all ${book.secondary_keys.join(',')}\n` + content
|
|
break
|
|
}
|
|
case 2:{
|
|
book.selective = false
|
|
for(const secKey of book.secondary_keys){
|
|
content = `@@exclude_keys ${secKey}\n` + content
|
|
}
|
|
break
|
|
}
|
|
case 3:{
|
|
book.selective = false
|
|
for(const secKey of book.secondary_keys){
|
|
content = `@@additional_keys ${secKey}\n` + content
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if(typeof extensions.delay === 'number' && extensions.delay > 0){
|
|
content = `@@activate_only_after ${extensions.delay}\n` + content
|
|
delete extensions.delay
|
|
}
|
|
if(extensions.match_whole_words === true){
|
|
content = `@@match_full_word\n` + content
|
|
delete extensions.match_whole_words
|
|
}
|
|
if(extensions.match_whole_words === false){
|
|
content = `@@match_partial_word\n` + content
|
|
delete extensions.match_whole_words
|
|
}
|
|
|
|
lorebook.push({
|
|
key: book.keys.join(', '),
|
|
secondkey: book.secondary_keys?.join(', ') ?? '',
|
|
insertorder: book.insertion_order,
|
|
comment: book.name ?? book.comment ?? "",
|
|
content: content,
|
|
mode: "normal",
|
|
alwaysActive: book.constant ?? false,
|
|
selective: book.selective ?? false,
|
|
extentions: {...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 {
|
|
lorebook,
|
|
loresettings,
|
|
loreExt
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function createBaseV2(char:character) {
|
|
|
|
let charBook:charBookEntry[] = []
|
|
for(const lore of char.globalLore){
|
|
let ext:{
|
|
risu_case_sensitive?: boolean;
|
|
risu_activationPercent?: number
|
|
risu_loreCache?: {
|
|
key:string
|
|
data:string[]
|
|
}
|
|
} = safeStructuredClone(lore.extentions ?? {})
|
|
|
|
let caseSensitive = ext.risu_case_sensitive ?? false
|
|
ext.risu_activationPercent = lore.activationPercent
|
|
ext.risu_loreCache = lore.loreCache
|
|
|
|
charBook.push({
|
|
keys: lore.key.split(',').map(r => r.trim()),
|
|
secondary_keys: lore.selective ? lore.secondkey.split(',').map(r => r.trim()) : undefined,
|
|
content: lore.content,
|
|
extensions: ext,
|
|
enabled: true,
|
|
insertion_order: lore.insertorder,
|
|
constant: lore.alwaysActive,
|
|
selective:lore.selective,
|
|
name: lore.comment,
|
|
comment: lore.comment,
|
|
case_sensitive: caseSensitive,
|
|
})
|
|
}
|
|
char.loreExt ??= {}
|
|
|
|
char.loreExt.risu_fullWordMatching = char.loreSettings?.fullWordMatching ?? false
|
|
|
|
const card:CharacterCardV2Risu = {
|
|
spec: "chara_card_v2",
|
|
spec_version: "2.0",
|
|
data: {
|
|
name: char.name,
|
|
description: char.desc ?? '',
|
|
personality: char.personality ?? '',
|
|
scenario: char.scenario ?? '',
|
|
first_mes: char.firstMessage ?? '',
|
|
mes_example: char.exampleMessage ?? '',
|
|
creator_notes: char.creatorNotes ?? '',
|
|
system_prompt: char.systemPrompt ?? '',
|
|
post_history_instructions: char.replaceGlobalNote ?? '',
|
|
alternate_greetings: char.alternateGreetings ?? [],
|
|
character_book: {
|
|
scan_depth: char.loreSettings?.scanDepth,
|
|
token_budget: char.loreSettings?.tokenBudget,
|
|
recursive_scanning: char.loreSettings?.recursiveScanning,
|
|
extensions: char.loreExt ?? {},
|
|
entries: charBook
|
|
},
|
|
tags: char.tags ?? [],
|
|
creator: char.additionalData?.creator ?? '',
|
|
character_version: `${char.additionalData?.character_version}` || '',
|
|
extensions: {
|
|
risuai: {
|
|
// emotions: char.emotionImages,
|
|
bias: char.bias,
|
|
viewScreen: char.viewScreen,
|
|
customScripts: char.customscript,
|
|
utilityBot: char.utilityBot,
|
|
sdData: char.sdData,
|
|
// additionalAssets: char.additionalAssets,
|
|
backgroundHTML: char.backgroundHTML,
|
|
license: char.license,
|
|
triggerscript: char.triggerscript,
|
|
additionalText: char.additionalText,
|
|
virtualscript: '', //removed dude to security issue
|
|
largePortrait: char.largePortrait,
|
|
lorePlus: char.lorePlus,
|
|
inlayViewScreen: char.inlayViewScreen,
|
|
newGenData: char.newGenData,
|
|
vits: {}
|
|
},
|
|
depth_prompt: char.depth_prompt
|
|
}
|
|
}
|
|
}
|
|
|
|
if(char.extentions){
|
|
for(const key in char.extentions){
|
|
if(key === 'risuai' || key === 'depth_prompt'){
|
|
continue
|
|
}
|
|
card.data.extensions[key] = char.extentions[key]
|
|
}
|
|
}
|
|
return card
|
|
}
|
|
|
|
|
|
export async function exportCharacterCard(char:character, type:'png'|'json'|'charx'|'charxJpeg' = 'png', arg:{
|
|
password?:string
|
|
writer?:LocalWriter|VirtualWriter,
|
|
spec?:'v2'|'v3'
|
|
} = {}) {
|
|
let img = await readImage(char.image)
|
|
const spec:'v2'|'v3' = arg.spec ?? 'v2' //backward compatibility
|
|
try{
|
|
char.image = ''
|
|
img = type === 'png' ? (await reencodeImage(img)) : img
|
|
const localWriter = arg.writer ?? (new LocalWriter())
|
|
if(!arg.writer && type !== 'json'){
|
|
const nameExt = {
|
|
'png': ['Image File', 'png'],
|
|
'json': ['JSON File', 'json'],
|
|
'charx': ['CharX File', 'charx'],
|
|
'charxJpeg': ['CharX Embeded Jpeg', 'jpeg']
|
|
}
|
|
const ext = nameExt[type]
|
|
console.log(ext)
|
|
await (localWriter as LocalWriter).init(ext[0], [ext[1]])
|
|
}
|
|
const writer = (type === 'charx' || type === 'charxJpeg') ? (new CharXWriter(localWriter)) : type === 'json' ? (new BlankWriter()) : (new PngChunk.streamWriter(img, localWriter))
|
|
await writer.init()
|
|
if(writer instanceof CharXWriter && type === 'charxJpeg'){
|
|
await writer.writeJpeg(img)
|
|
}
|
|
let assetIndex = 0
|
|
if(spec === 'v2'){
|
|
const card = await createBaseV2(char)
|
|
if(card.data.extensions.risuai.emotions && card.data.extensions.risuai.emotions.length > 0){
|
|
for(let i=0;i<card.data.extensions.risuai.emotions.length;i++){
|
|
alertStore.set({
|
|
type: 'progress',
|
|
msg: 'Loading... (Adding Emotions)',
|
|
submsg: (i / card.data.extensions.risuai.emotions.length * 100).toFixed(2)
|
|
})
|
|
const key = card.data.extensions.risuai.emotions[i][1]
|
|
const rData = await readImage(key)
|
|
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)
|
|
}
|
|
}
|
|
|
|
|
|
if(card.data.extensions.risuai.additionalAssets && card.data.extensions.risuai.additionalAssets.length > 0){
|
|
for(let i=0;i<card.data.extensions.risuai.additionalAssets.length;i++){
|
|
alertStore.set({
|
|
type: 'progress',
|
|
msg: 'Loading... (Adding Additional Assets)',
|
|
submsg: (i / card.data.extensions.risuai.additionalAssets.length * 100).toFixed(2)
|
|
})
|
|
const key = card.data.extensions.risuai.additionalAssets[i][1]
|
|
const rData = await readImage(key)
|
|
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)
|
|
}
|
|
}
|
|
|
|
if(char.vits && char.ttsMode === 'vits'){
|
|
const keys = Object.keys(char.vits.files)
|
|
for(let i=0;i<keys.length;i++){
|
|
alertStore.set({
|
|
type: 'progress',
|
|
msg: 'Loading... (Adding VITS)',
|
|
submsg: (i / keys.length * 100).toFixed(2)
|
|
})
|
|
const key = keys[i]
|
|
const rData = await loadAsset(char.vits.files[key])
|
|
const b64encoded = Buffer.from(rData).toString('base64')
|
|
assetIndex++
|
|
card.data.extensions.risuai.vits[key] = `__asset:${assetIndex}`
|
|
await writer.write("chara-ext-asset_:" + assetIndex, b64encoded)
|
|
}
|
|
}
|
|
if(type === 'json'){
|
|
await downloadFile(`${char.name.replace(/[<>:"/\\|?*\.\,]/g, "")}_export.json`, Buffer.from(JSON.stringify(card, null, 4), 'utf-8'))
|
|
alertNormal(language.successExport)
|
|
return
|
|
}
|
|
|
|
await sleep(10)
|
|
alertStore.set({
|
|
type: 'wait',
|
|
msg: 'Loading... (Writing)'
|
|
})
|
|
|
|
await writer.write("chara", Buffer.from(JSON.stringify(card)).toString('base64'))
|
|
}
|
|
else if(spec === 'v3'){
|
|
const card = createBaseV3(char)
|
|
if(card.data.assets && card.data.assets.length > 0){
|
|
for(let i=0;i<card.data.assets.length;i++){
|
|
alertStore.set({
|
|
type: 'progress',
|
|
msg: 'Loading... (Adding Assets)',
|
|
submsg: (i / card.data.assets.length * 100).toFixed(2)
|
|
})
|
|
let key = card.data.assets[i].uri
|
|
let rData:Uint8Array
|
|
if(key === 'ccdefault:' && type !== 'png'){
|
|
key = char.image
|
|
rData = img
|
|
}
|
|
else if(isKnownUri(key)){
|
|
continue
|
|
}
|
|
else{
|
|
rData = await readImage(key)
|
|
}
|
|
assetIndex++
|
|
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)
|
|
}
|
|
else if(type === 'json'){
|
|
const b64encoded = Buffer.from(await convertImage(rData)).toString('base64')
|
|
card.data.assets[i].uri = `data:application/octet-stream;base64,${b64encoded}`
|
|
}
|
|
else{
|
|
let type = 'other'
|
|
let itype = 'other'
|
|
switch(card.data.assets[i].type){
|
|
case 'emotion':
|
|
type = 'emotion'
|
|
break
|
|
case 'background':
|
|
type = 'background'
|
|
break
|
|
case 'user_icon':
|
|
type = 'user_icon'
|
|
break
|
|
case 'icon':
|
|
type = 'icon'
|
|
break
|
|
}
|
|
switch(card.data.assets[i].ext){
|
|
case 'png':
|
|
case 'jpg':
|
|
case 'jpeg':
|
|
case 'gif':
|
|
case 'webp':
|
|
case 'avif':
|
|
itype = 'image'
|
|
break
|
|
case 'mp3':
|
|
case 'wav':
|
|
case 'ogg':
|
|
case 'flac':
|
|
itype = 'audio'
|
|
break
|
|
case 'mp4':
|
|
case 'webm':
|
|
case 'mov':
|
|
case 'avi':
|
|
case 'mkv':
|
|
itype = 'video'
|
|
break
|
|
case 'mmd':
|
|
case 'obj':
|
|
itype = 'model'
|
|
break
|
|
case 'safetensors':
|
|
case 'cpkt':
|
|
case 'onnx':
|
|
itype = 'ai'
|
|
break
|
|
case 'otf':
|
|
case 'ttf':
|
|
case 'woff':
|
|
case 'woff2':
|
|
itype = 'fonts'
|
|
break
|
|
case 'js':
|
|
case 'ts':
|
|
case 'lua':
|
|
itype = 'code'
|
|
}
|
|
|
|
let path = ''
|
|
const name = `${assetIndex}`
|
|
if(card.data.assets[i].ext === 'unknown'){
|
|
path = `assets/${type}/image/${name}.png`
|
|
}
|
|
else{
|
|
path = `assets/${type}/${itype}/${name}.${card.data.assets[i].ext}`
|
|
}
|
|
card.data.assets[i].uri = 'embeded://' + path
|
|
const imageType = checkImageType(rData)
|
|
const metaPath = `x_meta/${name}.json`
|
|
if(imageType === 'PNG' && writer instanceof CharXWriter){
|
|
const metadatas:Record<string,string> = {}
|
|
const gen = PngChunk.readGenerator(rData)
|
|
for await (const chunk of gen){
|
|
if(!chunk || chunk instanceof AppendableBuffer){
|
|
continue
|
|
}
|
|
metadatas[chunk.key] = chunk.value
|
|
}
|
|
console.log(metadatas)
|
|
if(Object.keys(metadatas).length > 0){
|
|
await writer.write(metaPath, Buffer.from(JSON.stringify(metadatas, null, 4)), 6)
|
|
}
|
|
else{
|
|
await writer.write(metaPath, Buffer.from(JSON.stringify({
|
|
'type': imageType
|
|
}), 'utf-8'), 6)
|
|
}
|
|
}
|
|
else{
|
|
await writer.write(metaPath, Buffer.from(JSON.stringify({
|
|
'type': imageType
|
|
}), 'utf-8'), 6)
|
|
}
|
|
await writer.write(path, Buffer.from(await convertImage(rData)))
|
|
}
|
|
}
|
|
}
|
|
if(type === 'json'){
|
|
await downloadFile(`${char.name.replace(/[<>:"/\\|?*\.\,]/g, "")}_export.json`, Buffer.from(JSON.stringify(card, null, 4), 'utf-8'))
|
|
alertNormal(language.successExport)
|
|
return
|
|
}
|
|
|
|
await sleep(10)
|
|
alertStore.set({
|
|
type: 'wait',
|
|
msg: 'Loading... (Writing)'
|
|
})
|
|
|
|
if(type === 'charx' || type === 'charxJpeg'){
|
|
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{
|
|
await writer.write("ccv3", Buffer.from(JSON.stringify(card)).toString('base64'))
|
|
}
|
|
}
|
|
await writer.end()
|
|
|
|
await sleep(10)
|
|
|
|
if(!arg.writer){
|
|
alertNormal(language.successExport)
|
|
}
|
|
|
|
}
|
|
catch(e){
|
|
console.error(e, e.stack)
|
|
alertError(`${e}`)
|
|
}
|
|
}
|
|
|
|
export function createBaseV3(char:character){
|
|
|
|
let charBook:LorebookEntry[] = []
|
|
let assets:Array<{
|
|
type: string
|
|
uri: string
|
|
name: string
|
|
ext: string
|
|
}> = safeStructuredClone(char.ccAssets ?? [])
|
|
|
|
if(char.additionalAssets){
|
|
for(const asset of char.additionalAssets){
|
|
assets.push({
|
|
type: 'x-risu-asset',
|
|
uri: asset[1],
|
|
name: asset[0],
|
|
ext: asset[2] || 'png'
|
|
})
|
|
}
|
|
}
|
|
|
|
if(char.emotionImages){
|
|
for(const asset of char.emotionImages){
|
|
assets.push({
|
|
type: 'emotion',
|
|
uri: asset[1],
|
|
name: asset[0],
|
|
ext: 'png'
|
|
})
|
|
}
|
|
|
|
assets.push({
|
|
type: 'icon',
|
|
uri: 'ccdefault:',
|
|
name: 'main',
|
|
ext: 'png'
|
|
})
|
|
}
|
|
|
|
for(const lore of char.globalLore){
|
|
let ext:{
|
|
risu_case_sensitive?: boolean;
|
|
risu_activationPercent?: number
|
|
risu_loreCache?: {
|
|
key:string
|
|
data:string[]
|
|
}
|
|
} = safeStructuredClone(lore.extentions ?? {})
|
|
|
|
let caseSensitive = ext.risu_case_sensitive ?? false
|
|
ext.risu_activationPercent = lore.activationPercent
|
|
ext.risu_loreCache = lore.loreCache
|
|
|
|
charBook.push({
|
|
keys: lore.key.split(',').map(r => r.trim()),
|
|
secondary_keys: lore.selective ? lore.secondkey.split(',').map(r => r.trim()) : undefined,
|
|
content: lore.content,
|
|
extensions: ext,
|
|
enabled: true,
|
|
insertion_order: lore.insertorder,
|
|
constant: lore.alwaysActive,
|
|
selective:lore.selective,
|
|
name: lore.comment,
|
|
comment: lore.comment,
|
|
case_sensitive: caseSensitive,
|
|
use_regex: lore.useRegex ?? false,
|
|
})
|
|
}
|
|
char.loreExt ??= {}
|
|
|
|
char.loreExt.risu_fullWordMatching = char.loreSettings?.fullWordMatching ?? false
|
|
|
|
const card:CharacterCardV3 = {
|
|
spec: "chara_card_v3",
|
|
spec_version: "3.0",
|
|
data: {
|
|
name: char.name,
|
|
description: char.desc ?? '',
|
|
personality: char.personality ?? '',
|
|
scenario: char.scenario ?? '',
|
|
first_mes: char.firstMessage ?? '',
|
|
mes_example: char.exampleMessage ?? '',
|
|
creator_notes: char.creatorNotes ?? '',
|
|
system_prompt: char.systemPrompt ?? '',
|
|
post_history_instructions: char.replaceGlobalNote ?? '',
|
|
alternate_greetings: char.alternateGreetings ?? [],
|
|
character_book: {
|
|
scan_depth: char.loreSettings?.scanDepth,
|
|
token_budget: char.loreSettings?.tokenBudget,
|
|
recursive_scanning: char.loreSettings?.recursiveScanning,
|
|
extensions: char.loreExt ?? {},
|
|
entries: charBook
|
|
},
|
|
tags: char.tags ?? [],
|
|
creator: char.additionalData?.creator ?? '',
|
|
character_version: `${char.additionalData?.character_version}` || '',
|
|
extensions: {
|
|
risuai: {
|
|
bias: char.bias,
|
|
viewScreen: char.viewScreen,
|
|
customScripts: char.customscript,
|
|
utilityBot: char.utilityBot,
|
|
sdData: char.sdData,
|
|
backgroundHTML: char.backgroundHTML,
|
|
license: char.license,
|
|
triggerscript: char.triggerscript,
|
|
additionalText: char.additionalText,
|
|
virtualscript: '', //removed dude to security issue
|
|
largePortrait: char.largePortrait,
|
|
lorePlus: char.lorePlus,
|
|
inlayViewScreen: char.inlayViewScreen,
|
|
newGenData: char.newGenData,
|
|
vits: {},
|
|
lowLevelAccess: char.lowLevelAccess ?? false,
|
|
defaultVariables: char.defaultVariables ?? '',
|
|
},
|
|
depth_prompt: char.depth_prompt
|
|
},
|
|
group_only_greetings: char.group_only_greetings ?? [],
|
|
nickname: char.nickname ?? '',
|
|
source: char.source ?? [],
|
|
creation_date: char.creation_date ?? 0,
|
|
modification_date: Math.floor(Date.now() / 1000),
|
|
assets: assets
|
|
}
|
|
}
|
|
|
|
if(char.extentions){
|
|
for(const key in char.extentions){
|
|
if(key === 'risuai' || key === 'depth_prompt'){
|
|
continue
|
|
}
|
|
card.data.extensions[key] = char.extentions[key]
|
|
}
|
|
}
|
|
return card
|
|
}
|
|
|
|
|
|
export async function shareRisuHub2(char:character, arg:{
|
|
nsfw: boolean,
|
|
tag:string
|
|
license: string
|
|
anon: boolean,
|
|
update: boolean
|
|
}) {
|
|
try {
|
|
char = safeStructuredClone(char)
|
|
char.license = arg.license
|
|
let tagList = arg.tag.split(',')
|
|
|
|
if(arg.nsfw){
|
|
tagList.push("nsfw")
|
|
}
|
|
|
|
await alertWait("Uploading...")
|
|
|
|
|
|
let tags = tagList.filter((v, i) => {
|
|
return (!!v) && (tagList.indexOf(v) === i)
|
|
})
|
|
char.tags = tags
|
|
|
|
|
|
const writer = new VirtualWriter()
|
|
await exportCharacterCard(char, 'png', {writer: writer})
|
|
const dat = Buffer.from(writer.buf.buffer).toString('base64') + '&' + 'rt.png'
|
|
|
|
openURL(`https://realm.risuai.net/hub/realm/upload#filedata=${encodeURIComponent(dat)}`)
|
|
|
|
let testMode = true
|
|
if(testMode){
|
|
return
|
|
}
|
|
|
|
const fetchPromise = fetch(hubURL + '/hub/realm/upload', {
|
|
method: "POST",
|
|
body: writer.buf.buffer,
|
|
headers: {
|
|
"Content-Type": 'image/png',
|
|
"x-risu-api-version": "4",
|
|
"x-risu-token": getDatabase()?.account?.token,
|
|
'x-risu-username': arg.anon ? '' : (getDatabase()?.account?.id),
|
|
'x-risu-debug': 'true',
|
|
'x-risu-update-id': arg.update ? (char.realmId ?? 'null') : 'null'
|
|
}
|
|
})
|
|
|
|
|
|
const res = await fetchPromise
|
|
|
|
if(res.status !== 200){
|
|
alertError(await res.text())
|
|
}
|
|
else{
|
|
const resJSON = await res.json()
|
|
alertMd(resJSON.message)
|
|
const currentChar = getCurrentCharacter()
|
|
if(currentChar.type === 'group'){
|
|
return
|
|
}
|
|
currentChar.realmId = resJSON.id
|
|
setCurrentCharacter(currentChar)
|
|
}
|
|
} catch (error) {
|
|
alertError(`${error}`)
|
|
}
|
|
|
|
}
|
|
|
|
export type hubType = {
|
|
name:string
|
|
desc: string
|
|
download: string,
|
|
id: string,
|
|
img: string
|
|
tags: string[],
|
|
viewScreen: "none" | "emotion" | "imggen"
|
|
hasLore:boolean
|
|
hasEmotion:boolean
|
|
hasAsset:boolean
|
|
creator?:string
|
|
creatorName?:string
|
|
hot:number
|
|
license:string
|
|
authorname?:string
|
|
original?:string
|
|
type:string
|
|
hidden?:boolean
|
|
}
|
|
|
|
export let hubAdditionalHTML = ''
|
|
|
|
export async function getRisuHub(arg:{
|
|
search:string,
|
|
page:number,
|
|
nsfw:boolean
|
|
sort:string
|
|
}):Promise<hubType[]> {
|
|
try {
|
|
arg.search += ' __shared'
|
|
const stringArg = `search==${arg.search}&&page==${arg.page}&&nsfw==${arg.nsfw}&&sort==${arg.sort}&&web==${(!isNodeServer && !Capacitor.isNativePlatform() && !isTauri) ? 'web' : 'other'}`
|
|
|
|
const da = await fetch(hubURL + '/realm/' + encodeURIComponent(stringArg), {
|
|
headers: {
|
|
"x-risuai-info": appVer + ';' + (isNodeServer ? 'node' : (Capacitor.isNativePlatform() ? 'capacitor' : isTauri ? 'tauri' : 'web'))
|
|
}
|
|
})
|
|
if(da.status !== 200){
|
|
return []
|
|
}
|
|
const jso = await da.json()
|
|
if(Array.isArray(jso)){
|
|
return jso
|
|
}
|
|
hubAdditionalHTML = jso.additionalHTML || hubAdditionalHTML
|
|
return jso.cards
|
|
} catch (error) {
|
|
return[]
|
|
}
|
|
}
|
|
|
|
export async function downloadRisuHub(id:string, arg:{
|
|
forceRedirect?: boolean
|
|
} = {}) {
|
|
try {
|
|
if(!arg.forceRedirect){
|
|
if(!(await alertTOS())){
|
|
return
|
|
}
|
|
alertStore.set({
|
|
type: "wait",
|
|
msg: "Downloading..."
|
|
})
|
|
}
|
|
const res = await fetch("https://realm.risuai.net/api/v1/download/dynamic/" + id + '?cors=true', {
|
|
headers: {
|
|
"x-risu-api-version": "4"
|
|
}
|
|
})
|
|
if(res.status !== 200){
|
|
alertError(await res.text())
|
|
return
|
|
}
|
|
|
|
if(res.headers.get('content-type') === 'image/png' || res.headers.get('content-type') === 'application/zip' || res.headers.get('content-type') === 'application/charx'){
|
|
let db = getDatabase()
|
|
if(res.headers.get('content-type') === 'application/zip' || res.headers.get('content-type') === 'application/charx'){
|
|
console.log('zip')
|
|
await importCharacterProcess({
|
|
name: 'realm.charx',
|
|
data: new Uint8Array(await res.arrayBuffer()),
|
|
lightningRealmImport: db.lightningRealmImport,
|
|
})
|
|
}
|
|
else{
|
|
await importCharacterProcess({
|
|
name: 'realm.png',
|
|
data: res.body,
|
|
lightningRealmImport: db.lightningRealmImport,
|
|
})
|
|
}
|
|
checkCharOrder()
|
|
db = getDatabase()
|
|
if(db.characters[db.characters.length-1] && (db.goCharacterOnImport || arg.forceRedirect)){
|
|
const index = db.characters.length-1
|
|
characterFormatUpdate(index);
|
|
selectedCharID.set(index);
|
|
}
|
|
return
|
|
}
|
|
|
|
const result = await res.json()
|
|
const data:CharacterCardV3 = result.card
|
|
const img:string = result.img
|
|
|
|
data.data.extensions.risuRealmImportId = id
|
|
|
|
await importCharacterCardSpec(data, await getHubResources(img), 'hub')
|
|
checkCharOrder()
|
|
let db = getDatabase()
|
|
if(db.characters[db.characters.length-1] && (db.goCharacterOnImport || arg.forceRedirect)){
|
|
const index = db.characters.length-1
|
|
characterFormatUpdate(index);
|
|
selectedCharID.set(index);
|
|
alertStore.set({
|
|
type: 'none',
|
|
msg: ''
|
|
})
|
|
}
|
|
} catch (error) {
|
|
console.error(error)
|
|
console.log(error.stack)
|
|
alertError("Error while importing")
|
|
}
|
|
}
|
|
|
|
export async function getHubResources(id:string) {
|
|
const res = await fetch(`${hubURL}/resource/${id}`)
|
|
if(res.status !== 200){
|
|
throw (await res.text())
|
|
}
|
|
return Buffer.from(await (res).arrayBuffer())
|
|
}
|
|
|
|
export function isCharacterHasAssets(char:character|groupChat){
|
|
if(char.type === 'group'){
|
|
return false
|
|
}
|
|
|
|
if(char.additionalAssets && char.additionalAssets.length > 0){
|
|
return true
|
|
}
|
|
|
|
if(char.emotionImages && char.emotionImages.length > 0){
|
|
return true
|
|
}
|
|
|
|
if(char.ccAssets && char.ccAssets.length > 0){
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
|
|
type CharacterCardV2Risu = {
|
|
spec: 'chara_card_v2'
|
|
spec_version: '2.0' // May 8th addition
|
|
data: {
|
|
name: string
|
|
description: string
|
|
personality: string
|
|
scenario: string
|
|
first_mes: string
|
|
mes_example: string
|
|
creator_notes: string
|
|
system_prompt: string
|
|
post_history_instructions: string
|
|
alternate_greetings: string[]
|
|
character_book?: CharacterBook
|
|
tags: string[]
|
|
creator: string
|
|
character_version: string
|
|
extensions: {
|
|
risuai?:{
|
|
emotions?:[string, string][]
|
|
bias?:[string, number][],
|
|
viewScreen?: any,
|
|
customScripts?:customscript[]
|
|
utilityBot?: boolean,
|
|
sdData?:[string,string][],
|
|
additionalAssets?:[string,string,string][],
|
|
backgroundHTML?:string,
|
|
license?:string,
|
|
triggerscript?:triggerscript[]
|
|
private?:boolean
|
|
additionalText?:string
|
|
virtualscript?:string
|
|
largePortrait?:boolean
|
|
lorePlus?:boolean
|
|
inlayViewScreen?:boolean
|
|
newGenData?: {
|
|
prompt: string,
|
|
negative: string,
|
|
instructions: string,
|
|
emotionInstructions: string,
|
|
},
|
|
vits?: {[key:string]:string}
|
|
}
|
|
depth_prompt?: { depth: number, prompt: string }
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
interface OldTavernChar{
|
|
avatar: "none"
|
|
chat: string
|
|
create_date: string
|
|
description: string
|
|
first_mes: string
|
|
mes_example: string
|
|
name: string
|
|
personality: string
|
|
scenario: string
|
|
talkativeness: "0.5"
|
|
spec_version?: '1.0'
|
|
}
|
|
type CharacterBook = {
|
|
name?: string
|
|
description?: string
|
|
scan_depth?: number // agnai: "Memory: Chat History Depth"
|
|
token_budget?: number // agnai: "Memory: Context Limit"
|
|
recursive_scanning?: boolean // no agnai equivalent. whether entry content can trigger other entries
|
|
extensions: Record<string, any>
|
|
entries: Array<charBookEntry>
|
|
}
|
|
|
|
interface charBookEntry{
|
|
keys: Array<string>
|
|
content: string
|
|
extensions: Record<string, any>
|
|
enabled: boolean
|
|
insertion_order: number // if two entries inserted, lower "insertion order" = inserted higher
|
|
|
|
// FIELDS WITH NO CURRENT EQUIVALENT IN SILLY
|
|
name?: string // not used in prompt engineering
|
|
priority?: number // if token budget reached, lower priority value = discarded first
|
|
|
|
// FIELDS WITH NO CURRENT EQUIVALENT IN AGNAI
|
|
id?: number // not used in prompt engineering
|
|
comment?: string // not used in prompt engineering
|
|
selective?: boolean // if `true`, require a key from both `keys` and `secondary_keys` to trigger the entry
|
|
secondary_keys?: Array<string> // see field `selective`. ignored if selective == false
|
|
constant?: boolean // if true, always inserted in the prompt (within budget limit)
|
|
position?: 'before_char' | 'after_char' // whether the entry is placed before or after the character defs
|
|
case_sensitive?:boolean
|
|
use_regex?:boolean
|
|
}
|
|
|
|
interface RccCardMetaData{
|
|
usePassword?: boolean
|
|
} |