feat: add export via ccv3 charx and json
This commit is contained in:
@@ -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",
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user