import { get } from "svelte/store" import { alertConfirm, alertError, alertNormal, alertSelect, alertStore } from "./alert" import { DataBase, defaultSdDataFunc, type character, saveImage, setDatabase, type customscript, type loreSettings, type loreBook } from "./database" import { checkNullish, 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 { downloadFile, readImage } from "./globalApi" import { cloneDeep } from "lodash" export async function importCharacter() { try { const f = await selectSingleFile(['png', 'json']) if(!f){ return } 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: "", 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):Promise{ if(!card ||card.spec !== 'chara_card_v2'){ return false } const data = card.data const im = img ? await saveImage(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() 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: lore.extentions ?? {}, enabled: true, insertion_order: lore.insertorder, constant: lore.alwaysActive, selective:lore.selective, name: lore.comment, comment: lore.comment }) } 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.postHistoryInstructions, 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.additionalData?.tag ?? [], creator: char.additionalData?.creator ?? '', character_version: char.additionalData?.character_version ?? 0, extensions: { risuai: { emotions: char.emotionImages, bias: char.bias, viewScreen: char.viewScreen, customScripts: char.customscript, utilityBot: char.utilityBot, sdData: char.sdData } } } } if(card.data.extensions.risuai.emotions && card.data.extensions.risuai.emotions.length > 0){ for(let i=0;i:"/\\|?*\.\,]/g, "")}_export.png`, img) alertNormal(language.successExport) } catch(e){ alertError(`${e}`) } } 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: number extensions: { risuai?:{ emotions?:[string, string][] bias?:[string, number][], viewScreen?: "none" | "emotion" | "imggen", customScripts?:customscript[] utilityBot?: boolean, sdData?:[string,string][] } } } } interface OldTavernChar{ avatar: "none" chat: string create_date: string description: string first_mes: string mes_example: string name: string personality: "" scenario: "" 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 }