import { get } from "svelte/store" import { alertConfirm, alertError, alertMd, alertNormal, alertSelect, alertStore } from "./alert" import { DataBase, defaultSdDataFunc, type character, setDatabase, type customscript, type loreSettings, type loreBook } from "./storage/database" import { checkNullish, selectMultipleFile, selectSingleFile, sleep } from "./util" import { language } from "src/lang" import { encode as encodeMsgpack, decode as decodeMsgpack } from "@msgpack/msgpack"; import { v4 as uuidv4 } from 'uuid'; import exifr from 'exifr' import { PngMetadata } from "./exif" import { characterFormatUpdate } from "./characters" import { checkCharOrder, downloadFile, readImage, saveAsset } from "./storage/globalApi" import { cloneDeep } from "lodash" import { selectedCharID } from "./stores" import { convertImage } from "./parser" export const hubURL = import.meta.env.DEV ? "http://127.0.0.1:8787" : "https://sv.risuai.xyz" export async function importCharacter() { try { const files = await selectMultipleFile(['png', 'json']) if(!files){ return } for(const f of files){ await importCharacterProcess(f) checkCharOrder() } } catch (error) { alertError(`${error}`) return null } } async function importCharacterProcess(f:{ name: string; data: Uint8Array; }) { if(f.name.endsWith('json')){ const da = JSON.parse(Buffer.from(f.data).toString('utf-8')) if(await importSpecv2(da)){ let db = get(DataBase) return db.characters.length - 1 } if((da.char_name || da.name) && (da.char_persona || da.description) && (da.char_greeting || da.first_mes)){ let db = get(DataBase) db.characters.push(convertOldTavernAndJSON(da)) DataBase.set(db) alertNormal(language.importedCharacter) return } else{ alertError(language.errors.noData) return } } alertStore.set({ type: 'wait', msg: 'Loading... (Reading)' }) await sleep(10) const img = f.data const readed = (await exifr.parse(img, true)) if(readed.chara){ // standard spec v2 imports const charaData:CharacterCardV2 = JSON.parse(Buffer.from(readed.chara, 'base64').toString('utf-8')) if(await importSpecv2(charaData, img)){ let db = get(DataBase) return db.characters.length - 1 } } if(readed.risuai){ // old risu imports await sleep(10) const va = decodeMsgpack(Buffer.from(readed.risuai, 'base64')) as any if(va.type !== 101){ alertError(language.errors.noData) return } let char:character = va.data let db = get(DataBase) if(char.emotionImages && char.emotionImages.length > 0){ for(let i=0;i 0){ for(let i=0;i", name: char.name, personality: char.personality ?? "", scenario: char.scenario ?? "", talkativeness: "0.5" } await sleep(10) img = PngMetadata.write(img, { 'chara': Buffer.from(JSON.stringify(tavernData)).toString('base64'), 'risuai': data }) alertStore.set({ type: 'wait', msg: 'Loading... (Writing)' }) char.image = '' await sleep(10) await downloadFile(`${char.name.replace(/[<>:"/\\|?*\.\,]/g, "")}_export.png`, img) alertNormal(language.successExport) } catch(e){ alertError(`${e}`) } } async function importSpecv2(card:CharacterCardV2, img?:Uint8Array, mode?:'hub'|'normal'):Promise{ if(!card ||card.spec !== 'chara_card_v2'){ return false } if(!mode){ mode = 'normal' } const data = card.data const im = img ? await saveAsset(PngMetadata.filter(img)) : undefined let db = get(DataBase) const risuext = cloneDeep(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][] = [] if(risuext){ if(risuext.emotions){ for(let i=0;i 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, }) } const card:CharacterCardV2 = { 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 } } } } console.log(card) return card } export async function exportSpecV2(char:character) { let img = await readImage(char.image) try{ const card = await createBaseV2(char) if(card.data.extensions.risuai.emotions && card.data.extensions.risuai.emotions.length > 0){ for(let i=0;i 0){ for(let i=0;i:"/\\|?*\.\,]/g, "")}_export.png`, img) alertNormal(language.successExport) } catch(e){ alertError(`${e}`) } } export async function shareRisuHub(char:character, arg:{ nsfw: boolean, privateMode:boolean tag:string }) { char = cloneDeep(char) let tagList = arg.tag.split(',') if(arg.nsfw){ tagList.push("nsfw") } if(arg.privateMode){ tagList.push("private") } let tags = tagList.filter((v, i) => { return (!!v) && (tagList.indexOf(v) === i) }) char.tags = tags let img = await readImage(char.image) try{ const card = await createBaseV2(char) let resources:[string,string][] = [] if(card.data.extensions.risuai.emotions && card.data.extensions.risuai.emotions.length > 0){ for(let i=0;i 0){ for(let i=0;i { const da = await fetch(hubURL + '/hub/list', { method: "POST", body: JSON.stringify(arg ?? {}) }) if(da.status !== 200){ return [] } console.log(da) return da.json() } export async function downloadRisuHub(id:string) { alertStore.set({ type: "wait", msg: "Downloading..." }) const res = await fetch(hubURL + '/hub/get', { method: "POST", body: JSON.stringify({ id: id }) }) if(res.status !== 200){ alertError(await res.text()) } const result = await res.json() const data:CharacterCardV2 = result.card const img:string = result.img await importSpecv2(data, await getHubResources(img), 'hub') checkCharOrder() let db = get(DataBase) if(db.characters[db.characters.length-1]){ const index = db.characters.length-1 characterFormatUpdate(index); selectedCharID.set(index); } } 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()) } type CharacterCardV2 = { 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?: "none" | "emotion" | "imggen", customScripts?:customscript[] utilityBot?: boolean, sdData?:[string,string][], additionalAssets?:[string,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" } 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 entries: Array } interface charBookEntry{ keys: Array content: string extensions: Record 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 // 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 }