diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 9e97da1..a38a241 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,4 +1,5 @@ use crate::config::{AppConfig, AppConfigState, ProxyKind}; +use crate::dataset_meta::{load_dataset_meta, save_dataset_meta, DatasetMeta}; use tauri::State; use tauri::{AppHandle, WebviewWindow}; @@ -55,6 +56,16 @@ pub fn list_proxy_kind_options() -> Vec { ] } +#[tauri::command] +pub fn load_dataset_meta_by_dir(dataset_dir: String) -> Result { + load_dataset_meta(&dataset_dir).map_err(|e| format!("读取数据集元数据失败: {e}")) +} + +#[tauri::command] +pub fn save_dataset_meta_by_dir(dataset_dir: String, meta: DatasetMeta) -> Result<(), String> { + save_dataset_meta(&dataset_dir, &meta).map_err(|e| format!("保存数据集元数据失败: {e}")) +} + fn proxy_kind_value(kind: &ProxyKind) -> &'static str { match kind { ProxyKind::Http => "Http", diff --git a/src-tauri/src/dataset_meta.rs b/src-tauri/src/dataset_meta.rs new file mode 100644 index 0000000..162d19e --- /dev/null +++ b/src-tauri/src/dataset_meta.rs @@ -0,0 +1,63 @@ +use serde::{Deserialize, Serialize}; +use std::{fs, path::PathBuf}; + +const DATASET_META_FILE: &str = "meta.toml"; + +#[derive(Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DatasetMeta { + pub name: String, + pub target_model: String, + pub lora_type: String, + pub unified_image_size: bool, + pub unified_image_ratio: bool, + pub image_size: [u32; 2], + pub trigger_words: Vec, +} + +impl DatasetMeta { + pub fn validate(&self) -> anyhow::Result<()> { + if self.name.trim().is_empty() { + anyhow::bail!("数据集名称不能为空") + } + + if self.target_model.trim().is_empty() { + anyhow::bail!("目标模型不能为空") + } + + if self.image_size[0] == 0 || self.image_size[1] == 0 { + anyhow::bail!("imageSize 必须为正整数") + } + + Ok(()) + } +} + +fn dataset_meta_path(dataset_dir: &str) -> PathBuf { + PathBuf::from(dataset_dir).join(DATASET_META_FILE) +} + +pub fn save_dataset_meta(dataset_dir: &str, meta: &DatasetMeta) -> anyhow::Result<()> { + meta.validate()?; + + let meta_path = dataset_meta_path(dataset_dir); + if let Some(parent_dir) = meta_path.parent() { + fs::create_dir_all(parent_dir)?; + } + + let content = toml::to_string_pretty(meta)?; + fs::write(meta_path, content)?; + Ok(()) +} + +pub fn load_dataset_meta(dataset_dir: &str) -> anyhow::Result { + let meta_path = dataset_meta_path(dataset_dir); + if !meta_path.exists() { + anyhow::bail!("缺少 meta.toml") + } + + let content = fs::read_to_string(meta_path)?; + let meta: DatasetMeta = toml::from_str(&content)?; + meta.validate()?; + Ok(meta) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 66c95a5..289570e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,5 +1,6 @@ mod commands; mod config; +mod dataset_meta; use tauri::Manager; @@ -19,7 +20,9 @@ pub fn run() { commands::set_window_title, commands::load_app_config, commands::save_app_config, - commands::list_proxy_kind_options + commands::list_proxy_kind_options, + commands::load_dataset_meta_by_dir, + commands::save_dataset_meta_by_dir ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/lib/stores/dataset.ts b/src/lib/stores/dataset.ts index e0fe0b5..0735bb1 100644 --- a/src/lib/stores/dataset.ts +++ b/src/lib/stores/dataset.ts @@ -1,6 +1,5 @@ import type { DatasetMeta, ImageMeta } from "$lib/types/meta"; -import { join } from "@tauri-apps/api/path"; -import { writeTextFile } from "@tauri-apps/plugin-fs"; +import { saveDatasetMetaByDir } from "$lib/utils/dataset-meta"; import { get, writable } from "svelte/store"; export const openedDatasetDir = writable(null); @@ -25,8 +24,7 @@ export async function updateActiveDatasetMeta(updater: DatasetMetaUpdater): Prom const nextMeta = typeof updater === "function" ? updater(currentMeta) : { ...currentMeta, ...updater }; - const metaPath = await join(datasetDir, "meta.json"); - await writeTextFile(metaPath, JSON.stringify(nextMeta, null, 2)); + await saveDatasetMetaByDir(datasetDir, nextMeta); activeDatasetMeta.set(nextMeta); return nextMeta; diff --git a/src/lib/utils/dataset-meta.ts b/src/lib/utils/dataset-meta.ts new file mode 100644 index 0000000..b6e2f9b --- /dev/null +++ b/src/lib/utils/dataset-meta.ts @@ -0,0 +1,10 @@ +import type { DatasetMeta } from '$lib/types/meta'; +import { invoke } from '@tauri-apps/api/core'; + +export async function loadDatasetMetaByDir(datasetDir: string) { + return invoke('load_dataset_meta_by_dir', { datasetDir }); +} + +export async function saveDatasetMetaByDir(datasetDir: string, meta: DatasetMeta) { + await invoke('save_dataset_meta_by_dir', { datasetDir, meta }); +} diff --git a/src/routes/boot/+page.svelte b/src/routes/boot/+page.svelte index 57c6422..6a0c81f 100644 --- a/src/routes/boot/+page.svelte +++ b/src/routes/boot/+page.svelte @@ -4,15 +4,56 @@ import NewFolder from '$lib/components/icons/NewFolder.svelte'; import OpenFolder from '$lib/components/icons/OpenFolder.svelte'; import { openedDatasetDir } from '$lib/stores/dataset'; import { currentActivate } from '$lib/stores/navigate'; -import { isDatasetMeta } from '$lib/types/meta'; -import { join } from '@tauri-apps/api/path'; +import { loadDatasetMetaByDir } from '$lib/utils/dataset-meta'; import { message, open } from '@tauri-apps/plugin-dialog'; -import { readDir, readTextFile } from '@tauri-apps/plugin-fs'; afterNavigate(() => { currentActivate.set('start'); }); +function normalizeErrorMessage(error: unknown): string { + if (typeof error === 'string') { + return error; + } + + if (error instanceof Error) { + return error.message; + } + + return String(error); +} + +function resolveDatasetLoadPrompt(error: unknown): { title: string; text: string } { + const message = normalizeErrorMessage(error); + const detail = message.replace('读取数据集元数据失败:', '').trim(); + + if (detail.includes('缺少 meta.toml')) { + return { + title: 'Missing meta.toml', + text: 'The selected folder does not contain meta.toml. Please choose a valid dataset folder.', + }; + } + + if (detail.includes('TOML parse error')) { + return { + title: 'Invalid meta.toml', + text: 'meta.toml has invalid TOML syntax. Please fix the file and try again.', + }; + } + + if (detail.includes('不能为空') || detail.includes('imageSize 必须为正整数')) { + return { + title: 'Invalid Dataset Metadata', + text: `meta.toml content is invalid: ${detail}`, + }; + } + + return { + title: 'Invalid Dataset Folder', + text: `Failed to read dataset metadata: ${detail || 'Unknown error'}`, + }; +} + async function loadDataset() { try { const selectResult = await open({ @@ -34,25 +75,13 @@ async function loadDataset() { return; } - const entries = await readDir(selectedPath); - const hasMetaFile = entries.some((entry) => entry.name === 'meta.json'); - - if (!hasMetaFile) { - await message('The selected folder is not a valid dataset folder.', { + try { + await loadDatasetMetaByDir(selectedPath); + } catch (error) { + const prompt = resolveDatasetLoadPrompt(error); + await message(prompt.text, { kind: 'warning', - title: 'Invalid Dataset Folder', - }); - return; - } - - const metaPath = await join(selectedPath, 'meta.json'); - const metaText = await readTextFile(metaPath); - const parsedMeta: unknown = JSON.parse(metaText); - - if (!isDatasetMeta(parsedMeta)) { - await message('Dataset meta information is invalid.', { - kind: 'warning', - title: 'Invalid meta.json', + title: prompt.title, }); return; } diff --git a/src/routes/dataset/+page.ts b/src/routes/dataset/+page.ts index e64483a..058aad8 100644 --- a/src/routes/dataset/+page.ts +++ b/src/routes/dataset/+page.ts @@ -1,6 +1,7 @@ import { goto } from '$app/navigation'; import { activeDatasetImageMetas, activeDatasetMeta, openedDatasetDir } from '$lib/stores/dataset'; -import { isDatasetMeta, type ImageMeta } from '$lib/types/meta'; +import type { ImageMeta } from '$lib/types/meta'; +import { loadDatasetMetaByDir } from '$lib/utils/dataset-meta'; import { join } from '@tauri-apps/api/path'; import { readDir, readTextFile } from '@tauri-apps/plugin-fs'; import { isNil } from 'es-toolkit'; @@ -16,18 +17,8 @@ export const load = (async () => { } try { - const metaPath = await join(activeDataset, 'meta.json'); - const metaText = await readTextFile(metaPath); - const parsedMeta: unknown = JSON.parse(metaText); - - if (!isDatasetMeta(parsedMeta)) { - activeDatasetMeta.set(null); - activeDatasetImageMetas.set([]); - await goto('/boot'); - return {}; - } - - activeDatasetMeta.set(parsedMeta); + const meta = await loadDatasetMetaByDir(activeDataset); + activeDatasetMeta.set(meta); const entries = await readDir(activeDataset); const hasImagesMeta = entries.some((entry) => entry.name === 'images.json');