[feat] added spec v2 import/export
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { alertConfirm, alertError, alertNormal, alertStore } from "./alert"
|
import { alertConfirm, alertError, alertNormal, alertSelect, alertStore } from "./alert"
|
||||||
import { DataBase, defaultSdDataFunc, type character, saveImage, setDatabase, type customscript } from "./database"
|
import { DataBase, defaultSdDataFunc, type character, saveImage, setDatabase, type customscript, type loreSettings, type loreBook } from "./database"
|
||||||
import { checkNullish, selectSingleFile, sleep } from "./util"
|
import { checkNullish, selectSingleFile, sleep } from "./util"
|
||||||
import { language } from "src/lang"
|
import { language } from "src/lang"
|
||||||
import { encode as encodeMsgpack, decode as decodeMsgpack } from "@msgpack/msgpack";
|
import { encode as encodeMsgpack, decode as decodeMsgpack } from "@msgpack/msgpack";
|
||||||
@@ -11,7 +11,6 @@ import { characterFormatUpdate } from "./characters"
|
|||||||
import { downloadFile, readImage } from "./globalApi"
|
import { downloadFile, readImage } from "./globalApi"
|
||||||
import { cloneDeep } from "lodash"
|
import { cloneDeep } from "lodash"
|
||||||
|
|
||||||
type CharacterBook = null
|
|
||||||
|
|
||||||
export async function importCharacter() {
|
export async function importCharacter() {
|
||||||
try {
|
try {
|
||||||
@@ -198,7 +197,11 @@ function convertOldTavernAndJSON(charaData:OldTavernChar, imgp:string|undefined
|
|||||||
|
|
||||||
export async function exportChar(charaID:number) {
|
export async function exportChar(charaID:number) {
|
||||||
const db = get(DataBase)
|
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){
|
if(!char.image){
|
||||||
alertError('Image Required')
|
alertError('Image Required')
|
||||||
@@ -209,6 +212,12 @@ export async function exportChar(charaID:number) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sel = await alertSelect(['Export as Spec V2','Export as Old RisuCard'])
|
||||||
|
if(sel === '0'){
|
||||||
|
exportSpecV2(char)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
alertStore.set({
|
alertStore.set({
|
||||||
type: 'wait',
|
type: 'wait',
|
||||||
msg: 'Loading...'
|
msg: 'Loading...'
|
||||||
@@ -289,6 +298,7 @@ async function importSpecv2(card:CharacterCardV2, img?:Uint8Array):Promise<boole
|
|||||||
if(!card ||card.spec !== 'chara_card_v2'){
|
if(!card ||card.spec !== 'chara_card_v2'){
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = card.data
|
const data = card.data
|
||||||
const im = img ? await saveImage(PngMetadata.filter(img)) : undefined
|
const im = img ? await saveImage(PngMetadata.filter(img)) : undefined
|
||||||
let db = get(DataBase)
|
let db = get(DataBase)
|
||||||
@@ -320,6 +330,40 @@ async function importSpecv2(card:CharacterCardV2, img?:Uint8Array):Promise<boole
|
|||||||
sdData = risuext.sdData ?? sdData
|
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 = {
|
let char:character = {
|
||||||
name: data.name ?? '',
|
name: data.name ?? '',
|
||||||
firstMessage: data.first_mes ?? '',
|
firstMessage: data.first_mes ?? '',
|
||||||
@@ -335,7 +379,7 @@ async function importSpecv2(card:CharacterCardV2, img?:Uint8Array):Promise<boole
|
|||||||
image: im,
|
image: im,
|
||||||
emotionImages: emotions,
|
emotionImages: emotions,
|
||||||
bias: bias,
|
bias: bias,
|
||||||
globalLore: [], //lorebook
|
globalLore: lorebook, //lorebook
|
||||||
viewScreen: viewScreen,
|
viewScreen: viewScreen,
|
||||||
chaId: uuidv4(),
|
chaId: uuidv4(),
|
||||||
sdData: sdData,
|
sdData: sdData,
|
||||||
@@ -352,9 +396,17 @@ async function importSpecv2(card:CharacterCardV2, img?:Uint8Array):Promise<boole
|
|||||||
personality:data.personality ?? '',
|
personality:data.personality ?? '',
|
||||||
scenario:data.scenario ?? '',
|
scenario:data.scenario ?? '',
|
||||||
firstMsgIndex: -1,
|
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)
|
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 = {
|
type CharacterCardV2 = {
|
||||||
spec: 'chara_card_v2'
|
spec: 'chara_card_v2'
|
||||||
@@ -408,3 +557,33 @@ interface OldTavernChar{
|
|||||||
scenario: ""
|
scenario: ""
|
||||||
talkativeness: "0.5"
|
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',
|
mode: 'multiple'|'constant'|'normal',
|
||||||
alwaysActive: boolean
|
alwaysActive: boolean
|
||||||
selective:boolean
|
selective:boolean
|
||||||
|
extentions?:{}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface character{
|
export interface character{
|
||||||
@@ -255,6 +256,12 @@ export interface character{
|
|||||||
scenario:string
|
scenario:string
|
||||||
firstMsgIndex:number
|
firstMsgIndex:number
|
||||||
loreSettings?:loreSettings
|
loreSettings?:loreSettings
|
||||||
|
loreExt?:any
|
||||||
|
additionalData?: {
|
||||||
|
tag?:string[]
|
||||||
|
creator?:string
|
||||||
|
character_version?:number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -264,6 +271,7 @@ export interface loreSettings{
|
|||||||
recursiveScanning: boolean
|
recursiveScanning: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface groupChat{
|
export interface groupChat{
|
||||||
type: 'group'
|
type: 'group'
|
||||||
image?:string
|
image?:string
|
||||||
|
|||||||
Reference in New Issue
Block a user