feat: add export via ccv3 charx and json

This commit is contained in:
kwaroran
2024-06-02 23:38:55 +09:00
parent 958217839b
commit 8dff1a6cde
7 changed files with 154 additions and 23 deletions

View File

@@ -646,4 +646,5 @@ export const languageEnglish = {
index: "Index", index: "Index",
search: "Search", search: "Search",
goCharacterOnImport: "Go to Character on Realm Import", goCharacterOnImport: "Go to Character on Realm Import",
format: "Format",
} }

View File

@@ -21,7 +21,7 @@
let btn let btn
let input = '' let input = ''
let cardExportType = 'realm' let cardExportType = 'realm'
let cardExportPassword = '' let cardExportType2 = ''
let cardLicense = '' let cardLicense = ''
let generationInfoMenuIndex = 0 let generationInfoMenuIndex = 0
$: { $: {
@@ -33,7 +33,7 @@
} }
if($alertStore.type !== 'cardexport'){ if($alertStore.type !== 'cardexport'){
cardExportType = 'realm' cardExportType = 'realm'
cardExportPassword = '' cardExportType2 = ''
cardLicense = '' cardLicense = ''
} }
} }
@@ -383,8 +383,7 @@
type: 'none', type: 'none',
msg: JSON.stringify({ msg: JSON.stringify({
type: 'cancel', type: 'cancel',
password: cardExportPassword, type2: cardExportType2
license: cardLicense
}) })
}) })
}}> }}>
@@ -421,13 +420,20 @@
<button class="bg-bgcolor px-2 py-4 rounded-lg ml-2 flex-1" class:ring-1={cardExportType === 'realm'} on:click={() => {cardExportType = 'realm'}}>RisuRealm</button> <button class="bg-bgcolor px-2 py-4 rounded-lg ml-2 flex-1" class:ring-1={cardExportType === 'realm'} on:click={() => {cardExportType = 'realm'}}>RisuRealm</button>
{/if} {/if}
</div> </div>
{#if $alertStore.submsg === '' && cardExportType === ''}
<span class="text-textcolor mt-4">{language.format}</span>
<SelectInput bind:value={cardExportType2} className="mt-2">
<OptionInput value="">PNG</OptionInput>
<OptionInput value="json">JSON</OptionInput>
<OptionInput value="charx">CHARX</OptionInput>
</SelectInput>
{/if}
<Button className="mt-4" on:click={() => { <Button className="mt-4" on:click={() => {
alertStore.set({ alertStore.set({
type: 'none', type: 'none',
msg: JSON.stringify({ msg: JSON.stringify({
type: cardExportType, type: cardExportType,
password: cardExportPassword, type2: cardExportType2
license: cardLicense
}) })
}) })
}}>{cardExportType === 'realm' ? language.shareCloud : language.export}</Button> }}>{cardExportType === 'realm' ? language.shareCloud : language.export}</Button>

View File

@@ -222,8 +222,7 @@ export async function alertCardExport(type:string = ''){
return JSON.parse(get(alertStore).msg) as { return JSON.parse(get(alertStore).msg) as {
type: string, type: string,
password: string, type2: string,
license: string
} }
} }

View File

@@ -5,13 +5,14 @@ import { checkNullish, decryptBuffer, encryptBuffer, isKnownUri, selectFileByDom
import { language } from "src/lang" import { language } from "src/lang"
import { v4 as uuidv4, v4 } from 'uuid'; import { v4 as uuidv4, v4 } from 'uuid';
import { characterFormatUpdate } from "./characters" import { characterFormatUpdate } from "./characters"
import { AppendableBuffer, checkCharOrder, downloadFile, loadAsset, LocalWriter, openURL, readImage, saveAsset, VirtualWriter } from "./storage/globalApi" import { AppendableBuffer, BlankWriter, checkCharOrder, downloadFile, loadAsset, LocalWriter, openURL, readImage, saveAsset, VirtualWriter } from "./storage/globalApi"
import { CurrentCharacter, SettingsMenuIndex, ShowRealmFrameStore, selectedCharID, settingsOpen } from "./stores" import { CurrentCharacter, SettingsMenuIndex, ShowRealmFrameStore, selectedCharID, settingsOpen } from "./stores"
import { convertImage, hasher } from "./parser" import { convertImage, hasher } from "./parser"
import { CCardLib, type CharacterCardV3, type LorebookEntry } from '@risuai/ccardlib' import { CCardLib, type CharacterCardV3, type LorebookEntry } from '@risuai/ccardlib'
import { reencodeImage } from "./process/files/image" import { reencodeImage } from "./process/files/image"
import { PngChunk } from "./pngChunk" import { PngChunk } from "./pngChunk"
import type { OnnxModelFiles } from "./process/transformers" import type { OnnxModelFiles } from "./process/transformers"
import { CharXWriter } from "./process/processzip"
export const hubURL = "https://sv.risuai.xyz" export const hubURL = "https://sv.risuai.xyz"
@@ -335,7 +336,7 @@ export async function exportChar(charaID:number):Promise<string> {
const option = await alertCardExport() const option = await alertCardExport()
if(option.type === ''){ if(option.type === ''){
exportCharacterCard(char,'png', {spec: 'v3'}) exportCharacterCard(char, option.type2 === 'json' ? 'json' : (option.type2 === 'charx' ? 'charx' : 'png'), {spec: 'v3'})
} }
else if(option.type === 'ccv2'){ else if(option.type === 'ccv2'){
exportCharacterCard(char,'png', {spec: 'v2'}) exportCharacterCard(char,'png', {spec: 'v2'})
@@ -748,7 +749,7 @@ async function createBaseV2(char:character) {
} }
export async function exportCharacterCard(char:character, type:'png'|'json' = 'png', arg:{ export async function exportCharacterCard(char:character, type:'png'|'json'|'charx' = 'png', arg:{
password?:string password?:string
writer?:LocalWriter|VirtualWriter, writer?:LocalWriter|VirtualWriter,
spec?:'v2'|'v3' spec?:'v2'|'v3'
@@ -759,10 +760,17 @@ export async function exportCharacterCard(char:character, type:'png'|'json' = 'p
char.image = '' char.image = ''
img = await reencodeImage(img) img = await reencodeImage(img)
const localWriter = arg.writer ?? (new LocalWriter()) const localWriter = arg.writer ?? (new LocalWriter())
if(!arg.writer){ if(!arg.writer && type !== 'json'){
await (localWriter as LocalWriter).init(`Image file`, ['png']) const nameExt = {
'png': ['Image File', 'png'],
'json': ['JSON File', 'json'],
'charx': ['CharX File', 'charx']
} }
const writer = new PngChunk.streamWriter(img, localWriter) const ext = nameExt[type]
console.log(ext)
await (localWriter as LocalWriter).init(ext[0], [ext[1]])
}
const writer = type === 'charx' ? (new CharXWriter(localWriter)) : type === 'json' ? (new BlankWriter()) : (new PngChunk.streamWriter(img, localWriter))
await writer.init() await writer.init()
let assetIndex = 0 let assetIndex = 0
if(spec === 'v2'){ if(spec === 'v2'){
@@ -835,16 +843,56 @@ export async function exportCharacterCard(char:character, type:'png'|'json' = 'p
type: 'wait', type: 'wait',
msg: `Loading... (Adding Assets ${i} / ${card.data.assets.length})` msg: `Loading... (Adding Assets ${i} / ${card.data.assets.length})`
}) })
const key = card.data.assets[i].uri let key = card.data.assets[i].uri
if(isKnownUri(key)){ let rData:Uint8Array
if(key === 'ccdefault:' && type !== 'png'){
key = char.image
rData = img
}
else if(isKnownUri(key)){
continue continue
} }
const rData = await readImage(key) else{
const b64encoded = Buffer.from(await convertImage(rData)).toString('base64') rData = await readImage(key)
}
assetIndex++ assetIndex++
if(type === 'png'){
const b64encoded = Buffer.from(await convertImage(rData)).toString('base64')
card.data.assets[i].uri = `__asset:${assetIndex}` card.data.assets[i].uri = `__asset:${assetIndex}`
await writer.write("chara-ext-asset_" + assetIndex, b64encoded) await writer.write("chara-ext-asset_" + assetIndex, b64encoded)
} }
else if(type === 'json'){
const b64encoded = Buffer.from(await convertImage(rData)).toString('base64')
card.data.assets[i].uri = `data:application/octet-stream;base64,${b64encoded}`
}
else{
let type = 'other'
switch(card.data.assets[i].type){
case 'emotion':
type = 'emotion'
break
case 'background':
type = 'background'
break
case 'user_icon':
type = 'user_icon'
break
case 'icon':
type = 'icon'
break
}
let path = ''
const name = `${assetIndex}`
if(card.data.assets[i].ext === 'unknown'){
path = `assets/${type}/${name}.png`
}
else{
path = `assets/${type}/${name}.${card.data.assets[i].ext}`
}
card.data.assets[i].uri = 'embeded://' + path
await writer.write(path, rData)
}
}
} }
if(type === 'json'){ if(type === 'json'){
await downloadFile(`${char.name.replace(/[<>:"/\\|?*\.\,]/g, "")}_export.json`, Buffer.from(JSON.stringify(card, null, 4), 'utf-8')) await downloadFile(`${char.name.replace(/[<>:"/\\|?*\.\,]/g, "")}_export.json`, Buffer.from(JSON.stringify(card, null, 4), 'utf-8'))
@@ -858,8 +906,13 @@ export async function exportCharacterCard(char:character, type:'png'|'json' = 'p
msg: 'Loading... (Writing)' msg: 'Loading... (Writing)'
}) })
if(type === 'charx'){
await writer.write("card.json", Buffer.from(JSON.stringify(card, null, 4)))
}
else{
await writer.write("ccv3", Buffer.from(JSON.stringify(card)).toString('base64')) await writer.write("ccv3", Buffer.from(JSON.stringify(card)).toString('base64'))
} }
}
await writer.end() await writer.end()
await sleep(10) await sleep(10)

View File

@@ -34,7 +34,7 @@ class StreamChunkWriter{
} }
} }
} }
async write(key:string, val:string){ async write(key:string, val:string|Uint8Array){
const keyData = new TextEncoder().encode(key) const keyData = new TextEncoder().encode(key)
const value = Buffer.from(val) const value = Buffer.from(val)

View File

@@ -1,3 +1,5 @@
import { AppendableBuffer, type LocalWriter, type VirtualWriter } from "../storage/globalApi";
import * as fflate from "fflate";
export async function processZip(dataArray: Uint8Array): Promise<string> { export async function processZip(dataArray: Uint8Array): Promise<string> {
const jszip = await import("jszip"); const jszip = await import("jszip");
@@ -13,3 +15,58 @@ export async function processZip(dataArray: Uint8Array): Promise<string> {
throw new Error("No image found in ZIP file"); throw new Error("No image found in ZIP file");
} }
} }
export class CharXWriter{
zip:fflate.Zip
writeEnd:boolean = false
apb = new AppendableBuffer()
constructor(private writer:LocalWriter|WritableStreamDefaultWriter<Uint8Array>|VirtualWriter){
const handlerAsync = async (err:Error, dat:Uint8Array, final:boolean) => {
if(dat){
this.apb.append(dat)
}
if(final){
this.writeEnd = true
}
}
this.zip = new fflate.Zip()
this.zip.ondata = handlerAsync
}
async init(){
//do nothing, just to make compatible with other writer
}
async write(key:string,data:Uint8Array|string){
console.log('write',key)
let dat:Uint8Array
if(typeof data === 'string'){
dat = new TextEncoder().encode(data)
}
else{
dat = data
}
this.writeEnd = false
const file = new fflate.ZipDeflate(key, {
level: 0
});
await this.zip.add(file)
await file.push(dat, true)
await this.writer.write(this.apb.buffer)
this.apb.buffer = new Uint8Array(0)
if(this.writeEnd){
await this.writer.close()
}
}
async end(){
await this.zip.end()
await this.writer.write(this.apb.buffer)
this.apb.buffer = new Uint8Array(0)
if(this.writeEnd){
await this.writer.close()
}
}
}

View File

@@ -1144,7 +1144,7 @@ export function getModelMaxContext(model:string):number|undefined{
return undefined return undefined
} }
class TauriWriter{ export class TauriWriter{
path: string path: string
firstWrite: boolean = true firstWrite: boolean = true
constructor(path: string){ constructor(path: string){
@@ -1561,3 +1561,18 @@ export function updateHeightMode(){
break break
} }
} }
export class BlankWriter{
constructor(){
}
async init(){
//do nothing, just to make compatible with other writer
}
async write(key:string,data:Uint8Array|string){
//do nothing, just to make compatible with other writer
}
async end(){
//do nothing, just to make compatible with other writer
}
}