mirror of
https://github.com/vegu-ai/talemate.git
synced 2025-12-16 19:57:47 +01:00
json character card import should use new system
This commit is contained in:
@@ -104,6 +104,8 @@ def get_media_type_from_extension(file_extension: str) -> str:
|
|||||||
return "image/jpeg"
|
return "image/jpeg"
|
||||||
elif ext == ".webp":
|
elif ext == ".webp":
|
||||||
return "image/webp"
|
return "image/webp"
|
||||||
|
elif ext == ".json":
|
||||||
|
return "application/json"
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported file extension: {ext}")
|
raise ValueError(f"Unsupported file extension: {ext}")
|
||||||
|
|
||||||
|
|||||||
@@ -53,11 +53,16 @@
|
|||||||
<v-card elevation="0">
|
<v-card elevation="0">
|
||||||
<v-card-text style="padding-top: 0;">
|
<v-card-text style="padding-top: 0;">
|
||||||
<img
|
<img
|
||||||
v-if="fileData"
|
v-if="fileData && isImageFile"
|
||||||
:src="fileData"
|
:src="fileData"
|
||||||
alt="Character Card"
|
alt="Character Card"
|
||||||
style="width: 100%; max-width: 100%; max-height: 500px; height: auto; object-fit: contain; border-radius: 4px;"
|
style="width: 100%; max-width: 100%; max-height: 500px; height: auto; object-fit: contain; border-radius: 4px;"
|
||||||
/>
|
/>
|
||||||
|
<div v-else-if="fileData && !isImageFile" class="text-caption text-grey pa-4">
|
||||||
|
<v-icon size="large" color="grey">mdi-file-document-outline</v-icon>
|
||||||
|
<div class="mt-2">JSON Character Card</div>
|
||||||
|
<div class="text-caption mt-1">{{ filename || 'Character card file' }}</div>
|
||||||
|
</div>
|
||||||
<div v-else-if="filePath" class="text-caption text-grey pa-4">
|
<div v-else-if="filePath" class="text-caption text-grey pa-4">
|
||||||
Image preview not available for file path
|
Image preview not available for file path
|
||||||
</div>
|
</div>
|
||||||
@@ -77,11 +82,16 @@
|
|||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-card-text class="text-center" style="padding-top: 0;">
|
<v-card-text class="text-center" style="padding-top: 0;">
|
||||||
<img
|
<img
|
||||||
v-if="fileData"
|
v-if="fileData && isImageFile"
|
||||||
:src="fileData"
|
:src="fileData"
|
||||||
alt="Character Card"
|
alt="Character Card"
|
||||||
style="width: 100%; max-width: 100%; max-height: 800px; height: auto; object-fit: contain; border-radius: 4px;"
|
style="width: 100%; max-width: 100%; max-height: 800px; height: auto; object-fit: contain; border-radius: 4px;"
|
||||||
/>
|
/>
|
||||||
|
<div v-else-if="fileData && !isImageFile" class="text-caption text-grey pa-4">
|
||||||
|
<v-icon size="large" color="grey">mdi-file-document-outline</v-icon>
|
||||||
|
<div class="mt-2">JSON Character Card</div>
|
||||||
|
<div class="text-caption mt-1">{{ filename || 'Character card file' }}</div>
|
||||||
|
</div>
|
||||||
<div v-else-if="filePath" class="text-caption text-grey pa-4">
|
<div v-else-if="filePath" class="text-caption text-grey pa-4">
|
||||||
Image preview not available for file path
|
Image preview not available for file path
|
||||||
</div>
|
</div>
|
||||||
@@ -90,20 +100,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
<v-card elevation="0" class="mt-3">
|
|
||||||
<v-card-text>
|
|
||||||
<v-select
|
|
||||||
v-model="options.writing_style_template"
|
|
||||||
:items="writingStyleItems"
|
|
||||||
label="Writing Style"
|
|
||||||
hint="Optional: Select a writing style template to apply to the scene."
|
|
||||||
persistent-hint
|
|
||||||
clearable
|
|
||||||
density="compact"
|
|
||||||
variant="outlined"
|
|
||||||
></v-select>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="4">
|
<v-col cols="12" md="4">
|
||||||
<v-card elevation="0">
|
<v-card elevation="0">
|
||||||
@@ -215,7 +211,19 @@
|
|||||||
hint="If enabled, creates a shared context file and marks imported characters and world entries as shared."
|
hint="If enabled, creates a shared context file and marks imported characters and world entries as shared."
|
||||||
persistent-hint
|
persistent-hint
|
||||||
color="primary"
|
color="primary"
|
||||||
|
class="mb-2"
|
||||||
></v-switch>
|
></v-switch>
|
||||||
|
<v-divider class="my-3"></v-divider>
|
||||||
|
<v-select
|
||||||
|
v-model="options.writing_style_template"
|
||||||
|
:items="writingStyleItems"
|
||||||
|
label="Writing Style"
|
||||||
|
hint="Optional: Select a writing style template to apply to the scene."
|
||||||
|
persistent-hint
|
||||||
|
clearable
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
></v-select>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
@@ -368,6 +376,7 @@ export default {
|
|||||||
fileData: null,
|
fileData: null,
|
||||||
filePath: null,
|
filePath: null,
|
||||||
filename: null,
|
filename: null,
|
||||||
|
fileMediaType: null,
|
||||||
playerCharacterMode: 'template',
|
playerCharacterMode: 'template',
|
||||||
playerCharacterTemplate: {
|
playerCharacterTemplate: {
|
||||||
name: '',
|
name: '',
|
||||||
@@ -428,6 +437,8 @@ export default {
|
|||||||
this.fileData = fileData;
|
this.fileData = fileData;
|
||||||
this.filePath = filePath;
|
this.filePath = filePath;
|
||||||
this.filename = filename;
|
this.filename = filename;
|
||||||
|
// Reset media type - will be set from file_image_data response if needed
|
||||||
|
this.fileMediaType = null;
|
||||||
|
|
||||||
// Reset player character options
|
// Reset player character options
|
||||||
this.playerCharacterMode = 'template';
|
this.playerCharacterMode = 'template';
|
||||||
@@ -512,6 +523,7 @@ export default {
|
|||||||
} else if (data.image_data && data.file_path === this.filePath) {
|
} else if (data.image_data && data.file_path === this.filePath) {
|
||||||
// Only update if this is the current file path
|
// Only update if this is the current file path
|
||||||
this.fileData = data.image_data;
|
this.fileData = data.image_data;
|
||||||
|
this.fileMediaType = data.media_type || null;
|
||||||
}
|
}
|
||||||
} else if (data.type === 'scenes_list') {
|
} else if (data.type === 'scenes_list') {
|
||||||
this.scenes = data.data;
|
this.scenes = data.data;
|
||||||
@@ -629,6 +641,27 @@ export default {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
isImageFile() {
|
||||||
|
// Check media type first if available
|
||||||
|
if (this.fileMediaType) {
|
||||||
|
return this.fileMediaType.startsWith('image/');
|
||||||
|
}
|
||||||
|
// Check if fileData is actually an image data URL
|
||||||
|
if (this.fileData && typeof this.fileData === 'string' && this.fileData.startsWith('data:image/')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Also check filename extension as fallback
|
||||||
|
if (this.filename) {
|
||||||
|
const ext = this.filename.toLowerCase().split('.').pop();
|
||||||
|
return ['png', 'jpg', 'jpeg', 'webp'].includes(ext);
|
||||||
|
}
|
||||||
|
// Check filePath extension as last resort
|
||||||
|
if (this.filePath) {
|
||||||
|
const ext = this.filePath.toLowerCase().split('.').pop();
|
||||||
|
return ['png', 'jpg', 'jpeg', 'webp'].includes(ext);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
writingStyleItems() {
|
writingStyleItems() {
|
||||||
if (!this.templates || !this.templates.by_type || !this.templates.by_type.writing_style) {
|
if (!this.templates || !this.templates.by_type || !this.templates.by_type.writing_style) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
v-model="sceneFile"
|
v-model="sceneFile"
|
||||||
@update:modelValue="loadScene('file')"
|
@update:modelValue="loadScene('file')"
|
||||||
label="Drag and Drop file."
|
label="Drag and Drop file."
|
||||||
outlined accept="image/*"
|
outlined accept="image/*,.json"
|
||||||
variant="solo-filled"
|
variant="solo-filled"
|
||||||
messages="Upload a talemate scene or a character card"
|
messages="Upload a talemate scene or a character card"
|
||||||
></v-file-input>
|
></v-file-input>
|
||||||
@@ -87,6 +87,7 @@ export default {
|
|||||||
expanded: true,
|
expanded: true,
|
||||||
appConfig: null, // Store the app configuration
|
appConfig: null, // Store the app configuration
|
||||||
pendingLoadData: null, // Store pending load data while waiting for import options
|
pendingLoadData: null, // Store pending load data while waiting for import options
|
||||||
|
pendingJsonPath: null, // Store JSON file path while checking if it's a character card
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: {
|
emits: {
|
||||||
@@ -130,6 +131,97 @@ export default {
|
|||||||
this.sceneFile = [];
|
this.sceneFile = [];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Helper function to detect if JSON content is a character card
|
||||||
|
isCharacterCardJson(jsonData) {
|
||||||
|
if (!jsonData || typeof jsonData !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for character card spec versions
|
||||||
|
const spec = jsonData.spec;
|
||||||
|
if (spec === "chara_card_v1" || spec === "chara_card_v2" || spec === "chara_card_v3") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for v0 character card (has first_mes field)
|
||||||
|
if ("first_mes" in jsonData) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for v3+ character card (has first_mes in data object)
|
||||||
|
if (jsonData.data && typeof jsonData.data === 'object' && "first_mes" in jsonData.data) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
// Shared handler for processing JSON file content
|
||||||
|
async processJsonFile(jsonData, filePath, fileData, filename) {
|
||||||
|
try {
|
||||||
|
// Check if it's a character card
|
||||||
|
if (this.isCharacterCardJson(jsonData)) {
|
||||||
|
// Show character card import modal
|
||||||
|
const result = await this.showCharacterCardImportModal(fileData, filePath, filename);
|
||||||
|
if (!result || !result.confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Loading character card from JSON file");
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.$emit("loading", true);
|
||||||
|
|
||||||
|
if (fileData) {
|
||||||
|
// File upload - send as scene_data
|
||||||
|
this.getWebsocket().send(JSON.stringify({
|
||||||
|
type: 'load_scene',
|
||||||
|
scene_data: fileData,
|
||||||
|
filename: filename,
|
||||||
|
scene_initialization: {
|
||||||
|
character_card_import_options: result.options,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// Path-based - send file_path
|
||||||
|
this.getWebsocket().send(JSON.stringify({
|
||||||
|
type: 'load_scene',
|
||||||
|
file_path: filePath,
|
||||||
|
scene_initialization: {
|
||||||
|
character_card_import_options: result.options,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// It's a Talemate scene JSON, load it directly
|
||||||
|
console.log("Loading Talemate scene from JSON file");
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.$emit("loading", true);
|
||||||
|
|
||||||
|
if (fileData) {
|
||||||
|
// File upload - send as scene_data
|
||||||
|
this.getWebsocket().send(JSON.stringify({
|
||||||
|
type: 'load_scene',
|
||||||
|
scene_data: fileData,
|
||||||
|
filename: filename,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// Path-based - send file_path
|
||||||
|
this.getWebsocket().send(JSON.stringify({
|
||||||
|
type: 'load_scene',
|
||||||
|
file_path: filePath
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing JSON file:", error);
|
||||||
|
alert("Error: Invalid JSON file. " + error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async loadScene(inputMethod) {
|
async loadScene(inputMethod) {
|
||||||
if(this.sceneSaved === false) {
|
if(this.sceneSaved === false) {
|
||||||
if(!confirm("The current scene is not saved. Are you sure you want to load a new scene?")) {
|
if(!confirm("The current scene is not saved. Are you sure you want to load a new scene?")) {
|
||||||
@@ -177,6 +269,38 @@ export default {
|
|||||||
}));
|
}));
|
||||||
this.sceneFile = null; // Reset with null instead of empty array
|
this.sceneFile = null; // Reset with null instead of empty array
|
||||||
};
|
};
|
||||||
|
} else if (file.type === "application/json" || file.name.endsWith(".json")) {
|
||||||
|
// JSON file - check if it's a character card
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsText(file);
|
||||||
|
|
||||||
|
reader.onload = async () => {
|
||||||
|
try {
|
||||||
|
const jsonData = JSON.parse(reader.result);
|
||||||
|
|
||||||
|
// Convert to base64 for sending to backend
|
||||||
|
const base64Reader = new FileReader();
|
||||||
|
base64Reader.readAsDataURL(file);
|
||||||
|
|
||||||
|
base64Reader.onload = async () => {
|
||||||
|
const success = await this.processJsonFile(
|
||||||
|
jsonData,
|
||||||
|
null, // filePath (not used for uploads)
|
||||||
|
base64Reader.result,
|
||||||
|
file.name
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
this.sceneFile = null;
|
||||||
|
} else {
|
||||||
|
this.sceneFile = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error parsing JSON file:", error);
|
||||||
|
alert("Error: Invalid JSON file. " + error.message);
|
||||||
|
this.sceneFile = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
@@ -216,6 +340,16 @@ export default {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
this.sceneInput = '';
|
this.sceneInput = '';
|
||||||
|
} else if (this.sceneInput.endsWith(".json")) {
|
||||||
|
// JSON file - check if it's a character card
|
||||||
|
// Store the path for when we get the file data
|
||||||
|
this.pendingJsonPath = this.sceneInput;
|
||||||
|
// Request file content to check if it's a character card
|
||||||
|
this.getWebsocket().send(JSON.stringify({
|
||||||
|
type: 'request_file_image_data',
|
||||||
|
file_path: this.sceneInput,
|
||||||
|
}));
|
||||||
|
// Don't clear sceneInput yet - wait for file data response
|
||||||
} else {
|
} else {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.$emit("loading", true)
|
this.$emit("loading", true)
|
||||||
@@ -238,7 +372,7 @@ export default {
|
|||||||
this.getWebsocket().send(JSON.stringify(message));
|
this.getWebsocket().send(JSON.stringify(message));
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMessage(data) {
|
async handleMessage(data) {
|
||||||
// Handle app configuration
|
// Handle app configuration
|
||||||
if (data.type === 'app_config') {
|
if (data.type === 'app_config') {
|
||||||
this.appConfig = data.data;
|
this.appConfig = data.data;
|
||||||
@@ -267,6 +401,50 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle file_image_data - used for both images and JSON files
|
||||||
|
if (data.type === 'file_image_data') {
|
||||||
|
// Check if this is a pending JSON file check
|
||||||
|
if (this.pendingJsonPath && data.file_path === this.pendingJsonPath) {
|
||||||
|
const jsonPath = this.pendingJsonPath;
|
||||||
|
this.pendingJsonPath = null; // Clear pending path
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
console.error("Error loading JSON file:", data.error);
|
||||||
|
alert("Error loading file: " + data.error);
|
||||||
|
this.sceneInput = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.image_data && data.media_type === 'application/json') {
|
||||||
|
// Extract JSON text from base64 data URL
|
||||||
|
try {
|
||||||
|
// data.image_data is a data URL like "data:application/json;base64,..."
|
||||||
|
const base64Data = data.image_data.split(',')[1];
|
||||||
|
const jsonText = atob(base64Data);
|
||||||
|
const jsonData = JSON.parse(jsonText);
|
||||||
|
|
||||||
|
// Process JSON file (checks if character card and handles accordingly)
|
||||||
|
const success = await this.processJsonFile(
|
||||||
|
jsonData,
|
||||||
|
jsonPath,
|
||||||
|
data.image_data,
|
||||||
|
null // filename (not available for path-based)
|
||||||
|
);
|
||||||
|
if (success) {
|
||||||
|
this.sceneInput = '';
|
||||||
|
} else {
|
||||||
|
this.sceneInput = '';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error parsing JSON file:", error);
|
||||||
|
alert("Error: Invalid JSON file. " + error.message);
|
||||||
|
this.sceneInput = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
|||||||
Reference in New Issue
Block a user