[feat] added spec v2 import/export
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { get } from "svelte/store"
|
||||
import { alertConfirm, alertError, alertNormal, alertStore } from "./alert"
|
||||
import { DataBase, defaultSdDataFunc, type character, saveImage, setDatabase, type customscript } from "./database"
|
||||
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";
|
||||
@@ -11,7 +11,6 @@ import { characterFormatUpdate } from "./characters"
|
||||
import { downloadFile, readImage } from "./globalApi"
|
||||
import { cloneDeep } from "lodash"
|
||||
|
||||
type CharacterBook = null
|
||||
|
||||
export async function importCharacter() {
|
||||
try {
|
||||
@@ -198,7 +197,11 @@ function convertOldTavernAndJSON(charaData:OldTavernChar, imgp:string|undefined
|
||||
|
||||
export async function exportChar(charaID:number) {
|
||||
const db = get(DataBase)
|
||||
let char:character = JSON.parse(JSON.stringify(db.characters[charaID]))
|
||||
let char = cloneDeep(db.characters[charaID])
|
||||
|
||||
if(char.type === 'group'){
|
||||
return
|
||||
}
|
||||
|
||||
if(!char.image){
|
||||
alertError('Image Required')
|
||||
@@ -209,6 +212,12 @@ export async function exportChar(charaID:number) {
|
||||
return
|
||||
}
|
||||
|
||||
const sel = await alertSelect(['Export as Spec V2','Export as Old RisuCard'])
|
||||
if(sel === '0'){
|
||||
exportSpecV2(char)
|
||||
return
|
||||
}
|
||||
|
||||
alertStore.set({
|
||||
type: 'wait',
|
||||
msg: 'Loading...'
|
||||
@@ -289,6 +298,7 @@ async function importSpecv2(card:CharacterCardV2, img?:Uint8Array):Promise<boole
|
||||
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)
|
||||
@@ -320,6 +330,40 @@ async function importSpecv2(card:CharacterCardV2, img?:Uint8Array):Promise<boole
|
||||
sdData = risuext.sdData ?? sdData
|
||||
}
|
||||
|
||||
const charbook = data.character_book
|
||||
let lorebook:loreBook[] = []
|
||||
let loresettings:undefined|loreSettings = undefined
|
||||
let loreExt:undefined|any = undefined
|
||||
if(charbook){
|
||||
if((!checkNullish(charbook.recursive_scanning)) &&
|
||||
(!checkNullish(charbook.scan_depth)) &&
|
||||
(!checkNullish(charbook.token_budget))){
|
||||
loresettings = {
|
||||
tokenBudget:charbook.token_budget,
|
||||
scanDepth:charbook.scan_depth,
|
||||
recursiveScanning: charbook.recursive_scanning
|
||||
}
|
||||
}
|
||||
|
||||
loreExt = charbook.extensions
|
||||
|
||||
for(const book of charbook.entries){
|
||||
lorebook.push({
|
||||
key: book.keys.join(', '),
|
||||
secondkey: book.secondary_keys?.join(', ') ?? '',
|
||||
insertorder: book.insertion_order,
|
||||
comment: book.name ?? book.comment ?? "",
|
||||
content: book.content,
|
||||
mode: "normal",
|
||||
alwaysActive: book.constant ?? false,
|
||||
selective: book.selective ?? false,
|
||||
extentions: book.extensions
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
let char:character = {
|
||||
name: data.name ?? '',
|
||||
firstMessage: data.first_mes ?? '',
|
||||
@@ -335,7 +379,7 @@ async function importSpecv2(card:CharacterCardV2, img?:Uint8Array):Promise<boole
|
||||
image: im,
|
||||
emotionImages: emotions,
|
||||
bias: bias,
|
||||
globalLore: [], //lorebook
|
||||
globalLore: lorebook, //lorebook
|
||||
viewScreen: viewScreen,
|
||||
chaId: uuidv4(),
|
||||
sdData: sdData,
|
||||
@@ -352,9 +396,17 @@ async function importSpecv2(card:CharacterCardV2, img?:Uint8Array):Promise<boole
|
||||
personality:data.personality ?? '',
|
||||
scenario:data.scenario ?? '',
|
||||
firstMsgIndex: -1,
|
||||
removedQuotes: false
|
||||
removedQuotes: false,
|
||||
loreSettings: loresettings,
|
||||
loreExt: loreExt,
|
||||
additionalData: {
|
||||
tag: data.tags,
|
||||
creator: data.creator,
|
||||
character_version: data.character_version
|
||||
}
|
||||
}
|
||||
|
||||
db.characters.push(char)
|
||||
|
||||
setDatabase(db)
|
||||
|
||||
@@ -363,6 +415,103 @@ async function importSpecv2(card:CharacterCardV2, img?:Uint8Array):Promise<boole
|
||||
|
||||
}
|
||||
|
||||
export async function exportSpecV2(char:character) {
|
||||
let img = await readImage(char.image)
|
||||
|
||||
try{
|
||||
|
||||
let charBook:charBookEntry[] = []
|
||||
for(const lore of char.globalLore){
|
||||
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: 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: []
|
||||
},
|
||||
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<card.data.extensions.risuai.emotions.length;i++){
|
||||
alertStore.set({
|
||||
type: 'wait',
|
||||
msg: `Loading... (Getting Emotions ${i} / ${card.data.extensions.risuai.emotions.length})`
|
||||
})
|
||||
const rData = await readImage(card.data.extensions.risuai.emotions[i][1])
|
||||
char.emotionImages[i][1] = Buffer.from(rData).toString('base64')
|
||||
}
|
||||
}
|
||||
|
||||
alertStore.set({
|
||||
type: 'wait',
|
||||
msg: 'Loading... (Writing Exif)'
|
||||
})
|
||||
|
||||
await sleep(10)
|
||||
img = PngMetadata.write(img, {
|
||||
'chara': Buffer.from(JSON.stringify(card)).toString('base64'),
|
||||
})
|
||||
|
||||
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}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type CharacterCardV2 = {
|
||||
spec: 'chara_card_v2'
|
||||
@@ -408,3 +557,33 @@ interface OldTavernChar{
|
||||
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<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
|
||||
|
||||
}
|
||||
@@ -223,6 +223,7 @@ export interface loreBook{
|
||||
mode: 'multiple'|'constant'|'normal',
|
||||
alwaysActive: boolean
|
||||
selective:boolean
|
||||
extentions?:{}
|
||||
}
|
||||
|
||||
export interface character{
|
||||
@@ -255,6 +256,12 @@ export interface character{
|
||||
scenario:string
|
||||
firstMsgIndex:number
|
||||
loreSettings?:loreSettings
|
||||
loreExt?:any
|
||||
additionalData?: {
|
||||
tag?:string[]
|
||||
creator?:string
|
||||
character_version?:number
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -264,6 +271,7 @@ export interface loreSettings{
|
||||
recursiveScanning: boolean
|
||||
}
|
||||
|
||||
|
||||
export interface groupChat{
|
||||
type: 'group'
|
||||
image?:string
|
||||
|
||||
Reference in New Issue
Block a user