Add character card v3 import/export

This commit is contained in:
kwaroran
2024-05-25 09:23:59 +09:00
parent e5e7533e1a
commit 65fd56c0e4
7 changed files with 319 additions and 109 deletions

View File

@@ -8,7 +8,7 @@ import { characterFormatUpdate } from "./characters"
import { AppendableBuffer, checkCharOrder, downloadFile, loadAsset, LocalWriter, openURL, readImage, saveAsset, VirtualWriter } from "./storage/globalApi"
import { CurrentCharacter, selectedCharID } from "./stores"
import { convertImage, hasher } from "./parser"
import { CCardLib } from '@risuai/ccardlib'
import { CCardLib, type CharacterCardV3, type LorebookEntry } from '@risuai/ccardlib'
import { reencodeImage } from "./process/files/image"
import { PngChunk } from "./pngChunk"
import type { OnnxModelFiles } from "./process/transformers"
@@ -46,7 +46,7 @@ async function importCharacterProcess(f:{
}
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 importSpecv2(da)){
if(await importCharacterCardSpec(da)){
let db = get(DataBase)
return db.characters.length - 1
}
@@ -144,8 +144,8 @@ async function importCharacterProcess(f:{
else{
try {
const decrypted = await decryptBuffer(encrypted, password)
const charaData:CharacterCardV2 = JSON.parse(Buffer.from(decrypted).toString('utf-8'))
if(await importSpecv2(charaData, img, "normal", assets)){
const charaData:CharacterCardV2Risu = JSON.parse(Buffer.from(decrypted).toString('utf-8'))
if(await importCharacterCardSpec(charaData, img, "normal", assets)){
let db = get(DataBase)
return db.characters.length - 1
}
@@ -161,8 +161,8 @@ async function importCharacterProcess(f:{
else{
const decrypted = await decryptBuffer(encrypted, 'RISU_NONE')
try {
const charaData:CharacterCardV2 = JSON.parse(Buffer.from(decrypted).toString('utf-8'))
if(await importSpecv2(charaData, img, "normal", assets)){
const charaData:CharacterCardV2Risu = JSON.parse(Buffer.from(decrypted).toString('utf-8'))
if(await importCharacterCardSpec(charaData, img, "normal", assets)){
let db = get(DataBase)
return db.characters.length - 1
}
@@ -178,14 +178,7 @@ async function importCharacterProcess(f:{
const parsed = JSON.parse(Buffer.from(readedChara, 'base64').toString('utf-8'))
const checkedVersion = CCardLib.character.check(parsed)
if(checkedVersion === 'v2' || checkedVersion === 'v3'){
const charaData:CharacterCardV2 = CCardLib.character.convert(parsed, {
from: checkedVersion,
to: 'v2',
options: {
convertRisuFields: true
}
})
if(await importSpecv2(charaData, img, "normal", assets)){
if(await importCharacterCardSpec(parsed, img, "normal", assets)){
let db = get(DataBase)
return db.characters.length - 1
}
@@ -308,7 +301,10 @@ export async function exportChar(charaID:number):Promise<string> {
const option = await alertCardExport()
if(option.type === ''){
exportSpecV2(char,'png')
exportCharacterCard(char,'png', {spec: 'v3'})
}
else if(option.type === 'ccv2'){
exportCharacterCard(char,'png', {spec: 'v2'})
}
else{
return option.type
@@ -317,12 +313,13 @@ export async function exportChar(charaID:number):Promise<string> {
}
async function importSpecv2(card:CharacterCardV2, img?:Uint8Array, mode:'hub'|'normal' = 'normal', assetDict:{[key:string]:string} = {}):Promise<boolean>{
if(!card ||card.spec !== 'chara_card_v2'){
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
}
const data = card.data
console.log(card)
const im = img ? await saveAsset(await reencodeImage(img)) : undefined
let db = get(DataBase)
@@ -334,8 +331,15 @@ async function importSpecv2(card:CharacterCardV2, img?:Uint8Array, mode:'hub'|'n
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){
if(risuext && card.spec === 'chara_card_v2'){
if(risuext.emotions){
for(let i=0;i<risuext.emotions.length;i++){
alertStore.set({
@@ -417,6 +421,46 @@ async function importSpecv2(card:CharacterCardV2, img?:Uint8Array, mode:'hub'|'n
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: 'wait',
msg: `Loading... (Getting Assets ${i} / ${data.assets.length})`
})
await sleep(10)
let fileName = ''
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:', '')
const imgp = assetDict[key]
if(!imgp){
throw new Error('Error while importing, asset ' + key + ' not found')
}
extAssets.push([fileName,imgp,data.assets[i].ext])
continue
}
const imgp = await saveAsset(mode === 'hub' ? (await getHubResources(data.assets[i].uri)) :Buffer.from(data.assets[i].uri, 'base64'), '', fileName)
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{
ccAssets.push({
type: data.assets[i].type ?? 'asset',
uri: imgp,
name: fileName,
ext: data.assets[i].ext ?? 'unknown'
})
}
}
}
}
const charbook = data.character_book
let lorebook:loreBook[] = []
@@ -449,6 +493,8 @@ async function importSpecv2(card:CharacterCardV2, img?:Uint8Array, mode:'hub'|'n
extentions: {...book.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
})
}
@@ -521,6 +567,15 @@ async function importSpecv2(card:CharacterCardV2, img?:Uint8Array, mode:'hub'|'n
vits: vits,
ttsMode: vits ? 'vits' : 'normal',
imported: true,
source: card?.data?.extensions?.risuai?.source ?? [],
}
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)
@@ -534,6 +589,7 @@ async function importSpecv2(card:CharacterCardV2, img?:Uint8Array, mode:'hub'|'n
}
async function createBaseV2(char:character) {
let charBook:charBookEntry[] = []
@@ -569,7 +625,7 @@ async function createBaseV2(char:character) {
char.loreExt.risu_fullWordMatching = char.loreSettings?.fullWordMatching ?? false
const card:CharacterCardV2 = {
const card:CharacterCardV2Risu = {
spec: "chara_card_v2",
spec_version: "2.0",
data: {
@@ -630,15 +686,15 @@ async function createBaseV2(char:character) {
}
export async function exportSpecV2(char:character, type:'png'|'json'|'rcc' = 'png', arg:{
export async function exportCharacterCard(char:character, type:'png'|'json' = 'png', arg:{
password?:string
writer?:LocalWriter|VirtualWriter
writer?:LocalWriter|VirtualWriter,
spec?:'v2'|'v3'
} = {}) {
let img = await readImage(char.image)
const spec:'v2'|'v3' = arg.spec ?? 'v2' //backward compatibility
try{
char.image = ''
const card = await createBaseV2(char)
img = await reencodeImage(img)
const localWriter = arg.writer ?? (new LocalWriter())
if(!arg.writer){
@@ -647,81 +703,98 @@ export async function exportSpecV2(char:character, type:'png'|'json'|'rcc' = 'pn
const writer = new PngChunk.streamWriter(img, localWriter)
await writer.init()
let assetIndex = 0
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... (Adding Emotions ${i} / ${card.data.extensions.risuai.emotions.length})`
})
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(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: 'wait',
msg: `Loading... (Adding Emotions ${i} / ${card.data.extensions.risuai.emotions.length})`
})
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: 'wait',
msg: `Loading... (Adding Additional Assets ${i} / ${card.data.extensions.risuai.additionalAssets.length})`
})
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(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: 'wait',
msg: `Loading... (Adding Additional Assets ${i} / ${card.data.extensions.risuai.additionalAssets.length})`
})
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: 'wait',
msg: `Loading... (Adding VITS ${i} / ${keys.length})`
})
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(char.vits && char.ttsMode === 'vits'){
const keys = Object.keys(char.vits.files)
for(let i=0;i<keys.length;i++){
alertStore.set({
type: 'wait',
msg: `Loading... (Adding VITS ${i} / ${keys.length})`
})
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)'
})
if(type === 'rcc'){
const password = arg.password || 'RISU_NONE'
const json = JSON.stringify(card)
const encrypted = Buffer.from(await encryptBuffer(Buffer.from(json, 'utf-8'), password))
const hashed = await hasher(encrypted)
const metaData:RccCardMetaData = {}
if(password !== 'RISU_NONE'){
metaData.usePassword = true
if(type === 'json'){
await downloadFile(`${char.name.replace(/[<>:"/\\|?*\.\,]/g, "")}_export.json`, Buffer.from(JSON.stringify(card, null, 4), 'utf-8'))
alertNormal(language.successExport)
return
}
const rccString = 'rcc||rccv1||' + encrypted.toString('base64') + '||' + hashed + '||' + Buffer.from(JSON.stringify(metaData)).toString('base64')
await writer.write("chara", rccString)
}
else{
await writer.write("chara", Buffer.from(JSON.stringify(card)).toString('base64'))
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: 'wait',
msg: `Loading... (Adding Assets ${i} / ${card.data.assets.length})`
})
const key = card.data.assets[i].uri
const rData = await readImage(key)
const b64encoded = Buffer.from(await convertImage(rData)).toString('base64')
assetIndex++
card.data.assets[i].uri = `__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("ccv3", Buffer.from(JSON.stringify(card)).toString('base64'))
}
await writer.end()
await sleep(10)
@@ -737,9 +810,130 @@ export async function exportSpecV2(char:character, type:'png'|'json'|'rcc' = 'pn
}
}
export async function shareRisuHub3() {
export function createBaseV3(char:character){
let charBook:LorebookEntry[] = []
let assets:Array<{
type: string
uri: string
name: string
ext: string
}> = structuredClone(char.ccAssets ?? [])
for(const asset of char.additionalAssets){
assets.push({
type: 'x-risu-asset',
uri: asset[1],
name: asset[0],
ext: asset[2] || 'unknown'
})
}
for(const asset of char.emotionImages){
assets.push({
type: 'emotion',
uri: asset[1],
name: asset[0],
ext: 'unknown'
})
}
for(const lore of char.globalLore){
let ext:{
risu_case_sensitive?: boolean;
risu_activationPercent?: number
risu_loreCache?: {
key:string
data:string[]
}
} = structuredClone(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: {}
},
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
}
@@ -769,7 +963,7 @@ export async function shareRisuHub2(char:character, arg:{
const writer = new VirtualWriter()
await exportSpecV2(char, 'png', {writer: writer})
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)}`)
@@ -881,10 +1075,10 @@ export async function downloadRisuHub(id:string) {
}
const result = await res.json()
const data:CharacterCardV2 = result.card
const data:CharacterCardV2Risu = result.card
const img:string = result.img
await importSpecv2(data, await getHubResources(img), 'hub')
await importCharacterCardSpec(data, await getHubResources(img), 'hub')
checkCharOrder()
let db = get(DataBase)
if(db.characters[db.characters.length-1]){
@@ -908,7 +1102,7 @@ export async function getHubResources(id:string) {
type CharacterCardV2 = {
type CharacterCardV2Risu = {
spec: 'chara_card_v2'
spec_version: '2.0' // May 8th addition
data: {

View File

@@ -1,5 +1,5 @@
import { get } from "svelte/store";
import { exportSpecV2 } from "./characterCards";
import { exportCharacterCard } from "./characterCards";
import { VirtualWriter, isTauri, openURL } from "./storage/globalApi";
import { sleep } from "./util";
import { CurrentCharacter } from "./stores";
@@ -21,7 +21,7 @@ export async function shareRealmCardData():Promise<{ name: ArrayBuffer; data: Ar
const trimedName = char.name.replace(/[^a-zA-Z0-9]/g, '') || 'character';
const writer = new VirtualWriter()
const namebuf = new TextEncoder().encode(trimedName + '.png')
await exportSpecV2(char, 'png', {writer: writer})
await exportCharacterCard(char, 'png', {writer: writer})
alertStore.set({
type: 'none',
msg: ''

View File

@@ -687,7 +687,8 @@ export interface loreBook{
loreCache?:{
key:string
data:string[]
}
},
useRegex?:boolean
}
export interface character{
@@ -773,6 +774,17 @@ export interface character{
realmId?:string
imported?:boolean
trashTime?:number
nickname?:string
source?:string[]
group_only_greetings?:string[]
creation_date?:number
modification_date?:number
ccAssets?: Array<{
type: string
uri: string
name: string
ext: string
}>
}