Spaces:
Running
Running
Upload folder via script
Browse files- velai/static/css/velai.css +68 -0
- velai/ui/training.py +68 -71
velai/static/css/velai.css
CHANGED
|
@@ -14,4 +14,72 @@
|
|
| 14 |
|
| 15 |
.vue-flow__resize-control.line {
|
| 16 |
border-color: transparent !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
}
|
|
|
|
| 14 |
|
| 15 |
.vue-flow__resize-control.line {
|
| 16 |
border-color: transparent !important;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
/* LoRA dialog — match canvas graphite grey (#1c1d20), fit without scroll */
|
| 20 |
+
.velai-lora-dialog {
|
| 21 |
+
background: #1c1d20 !important;
|
| 22 |
+
color: #e5e7eb !important;
|
| 23 |
+
border: 1px solid #3c3e43;
|
| 24 |
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.55);
|
| 25 |
+
max-height: min(92vh, 720px) !important;
|
| 26 |
+
overflow: hidden !important;
|
| 27 |
+
padding: 12px 16px !important;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.velai-lora-dialog .q-card__section,
|
| 31 |
+
.velai-lora-dialog > .q-card__section--vert {
|
| 32 |
+
padding: 0;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.velai-lora-train-body {
|
| 36 |
+
max-height: calc(min(92vh, 720px) - 96px);
|
| 37 |
+
overflow: hidden;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.velai-lora-upload .q-uploader__list {
|
| 41 |
+
max-height: 48px;
|
| 42 |
+
overflow: hidden;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.velai-lora-dialog .q-tab-panels,
|
| 46 |
+
.velai-lora-dialog .q-tab-panel {
|
| 47 |
+
background: transparent !important;
|
| 48 |
+
color: inherit;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.velai-lora-dialog .q-tabs {
|
| 52 |
+
background: transparent;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.velai-lora-dialog .q-tab {
|
| 56 |
+
color: #9ca3af;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.velai-lora-dialog .q-tab--active {
|
| 60 |
+
color: #e5e7eb;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
.velai-lora-dialog .q-field__control,
|
| 64 |
+
.velai-lora-dialog .q-field__native,
|
| 65 |
+
.velai-lora-dialog .q-field__label {
|
| 66 |
+
color: #e5e7eb;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.velai-lora-dialog .q-uploader {
|
| 70 |
+
background: #2b2c2f;
|
| 71 |
+
color: #e5e7eb;
|
| 72 |
+
border: 1px solid #3c3e43;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.velai-lora-log {
|
| 76 |
+
background: #232427 !important;
|
| 77 |
+
color: #e5e7eb !important;
|
| 78 |
+
border: 1px solid #3c3e43;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.velai-lora-entry {
|
| 82 |
+
background: #2b2c2f !important;
|
| 83 |
+
color: #e5e7eb !important;
|
| 84 |
+
border: 1px solid #3c3e43;
|
| 85 |
}
|
velai/ui/training.py
CHANGED
|
@@ -22,74 +22,57 @@ class TrainingDialog:
|
|
| 22 |
self.zip_name: str = ""
|
| 23 |
self.uploaded_files: List[tuple[str, bytes]] = []
|
| 24 |
self.is_ready_to_train = False
|
| 25 |
-
self._advanced_open = False
|
| 26 |
self.on_library_changed = on_library_changed
|
| 27 |
|
| 28 |
self.dialog = ui.dialog().props("persistent")
|
| 29 |
|
| 30 |
-
with self.dialog, ui.card().classes("w-[
|
| 31 |
-
with ui.row().classes("w-full items-center justify-between"):
|
| 32 |
with ui.column().classes("gap-0"):
|
| 33 |
-
ui.label("Train / Manage LoRA").classes("text-
|
| 34 |
-
ui.label("Krea 2 Trainer").classes("text-
|
| 35 |
ui.button(icon="close", on_click=self.dialog.close).props("flat round dense")
|
| 36 |
|
| 37 |
with ui.tabs().classes("w-full") as tabs:
|
| 38 |
train_tab = ui.tab("Train")
|
| 39 |
library_tab = ui.tab("My LoRAs")
|
| 40 |
|
| 41 |
-
with ui.tab_panels(tabs, value=train_tab).classes("w-full"):
|
| 42 |
-
with ui.tab_panel(train_tab):
|
| 43 |
self._build_train_tab()
|
| 44 |
-
with ui.tab_panel(library_tab):
|
| 45 |
self._build_library_tab()
|
| 46 |
|
| 47 |
def _build_train_tab(self) -> None:
|
| 48 |
-
ui.
|
| 49 |
-
ui.label(
|
| 50 |
-
f"Zip of PNG/JPG/WebP images. Optional same-stem .txt captions "
|
| 51 |
-
f"(e.g. 001.jpg + 001.txt). Max {MAX_DATASET_ZIP_MB} MB "
|
| 52 |
-
f"(fal accepts larger archives; we cap uploads here for reliability)."
|
| 53 |
-
).classes("text-xs text-gray-500 mb-2")
|
| 54 |
-
|
| 55 |
-
self.upload = (
|
| 56 |
-
ui.upload(
|
| 57 |
-
label="Upload zip",
|
| 58 |
-
auto_upload=True,
|
| 59 |
-
on_upload=self.handle_upload,
|
| 60 |
-
multiple=False,
|
| 61 |
-
)
|
| 62 |
-
.props("accept=.zip,application/zip")
|
| 63 |
-
.classes("w-full")
|
| 64 |
-
)
|
| 65 |
-
|
| 66 |
-
self.file_list_label = ui.label("No zip uploaded yet").classes("text-xs text-gray-500 mt-1")
|
| 67 |
-
|
| 68 |
-
ui.label("2. Configuration").classes("font-bold mt-4")
|
| 69 |
-
with ui.grid(columns=2).classes("w-full gap-4"):
|
| 70 |
-
self.lora_name = ui.input("LoRA name", value="My Krea LoRA").classes("w-full")
|
| 71 |
-
self.steps = ui.select(
|
| 72 |
-
{1000: "1000 steps (default)", 2000: "2000 steps"},
|
| 73 |
-
value=1000,
|
| 74 |
-
label="Training steps",
|
| 75 |
-
).classes("w-full")
|
| 76 |
-
self.trigger_phrase = ui.input(
|
| 77 |
-
"Trigger phrase",
|
| 78 |
-
placeholder="e.g. ohwx person",
|
| 79 |
-
).classes("w-full col-span-2")
|
| 80 |
ui.label(
|
| 81 |
-
"
|
| 82 |
-
).classes("text-xs text-gray-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
value=768,
|
| 91 |
-
label="Resolution",
|
| 92 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
self.auto_captioning = ui.select(
|
| 94 |
{
|
| 95 |
"Off": "Off",
|
|
@@ -99,29 +82,43 @@ class TrainingDialog:
|
|
| 99 |
},
|
| 100 |
value="Off",
|
| 101 |
label="Auto captioning",
|
| 102 |
-
).classes("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
|
|
|
| 106 |
|
| 107 |
-
|
| 108 |
-
|
| 109 |
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
def _build_library_tab(self) -> None:
|
| 121 |
-
ui.label("LoRAs linked to this browser session").classes("text-sm text-gray-
|
| 122 |
ui.label(
|
| 123 |
"They persist across reloads via your session cookie. Upload a .safetensors file or train a new one."
|
| 124 |
-
).classes("text-xs text-gray-
|
| 125 |
|
| 126 |
self.lora_upload = (
|
| 127 |
ui.upload(
|
|
@@ -145,17 +142,17 @@ class TrainingDialog:
|
|
| 145 |
entries = list_loras(self._user_storage())
|
| 146 |
with self.library_container:
|
| 147 |
if not entries:
|
| 148 |
-
ui.label("No LoRAs yet. Train one or upload weights.").classes("text-sm text-gray-
|
| 149 |
return
|
| 150 |
for entry in entries:
|
| 151 |
-
with ui.card().classes("w-full"):
|
| 152 |
with ui.row().classes("w-full items-center justify-between"):
|
| 153 |
with ui.column().classes("gap-0"):
|
| 154 |
ui.label(entry.name).classes("font-bold")
|
| 155 |
meta = f"{entry.source} · {entry.created_at[:19]}"
|
| 156 |
if entry.trigger_phrase:
|
| 157 |
meta += f" · trigger: {entry.trigger_phrase}"
|
| 158 |
-
ui.label(meta).classes("text-xs text-gray-
|
| 159 |
with ui.row().classes("gap-1"):
|
| 160 |
ui.button(
|
| 161 |
icon="download",
|
|
@@ -309,7 +306,7 @@ class TrainingDialog:
|
|
| 309 |
original_name=lora_file.get("file_name") or "krea_lora.safetensors",
|
| 310 |
mime_type="application/octet-stream",
|
| 311 |
)
|
| 312 |
-
name = (self.lora_name.value or "My
|
| 313 |
add_lora(
|
| 314 |
self._user_storage(),
|
| 315 |
name=name,
|
|
|
|
| 22 |
self.zip_name: str = ""
|
| 23 |
self.uploaded_files: List[tuple[str, bytes]] = []
|
| 24 |
self.is_ready_to_train = False
|
|
|
|
| 25 |
self.on_library_changed = on_library_changed
|
| 26 |
|
| 27 |
self.dialog = ui.dialog().props("persistent")
|
| 28 |
|
| 29 |
+
with self.dialog, ui.card().classes("w-[720px] max-w-[95vw] velai-lora-dialog"):
|
| 30 |
+
with ui.row().classes("w-full items-center justify-between mb-1"):
|
| 31 |
with ui.column().classes("gap-0"):
|
| 32 |
+
ui.label("Train / Manage LoRA").classes("text-lg font-bold")
|
| 33 |
+
ui.label("Krea 2 Trainer").classes("text-xs text-gray-400")
|
| 34 |
ui.button(icon="close", on_click=self.dialog.close).props("flat round dense")
|
| 35 |
|
| 36 |
with ui.tabs().classes("w-full") as tabs:
|
| 37 |
train_tab = ui.tab("Train")
|
| 38 |
library_tab = ui.tab("My LoRAs")
|
| 39 |
|
| 40 |
+
with ui.tab_panels(tabs, value=train_tab).classes("w-full bg-transparent"):
|
| 41 |
+
with ui.tab_panel(train_tab).classes("p-0"):
|
| 42 |
self._build_train_tab()
|
| 43 |
+
with ui.tab_panel(library_tab).classes("p-0"):
|
| 44 |
self._build_library_tab()
|
| 45 |
|
| 46 |
def _build_train_tab(self) -> None:
|
| 47 |
+
with ui.column().classes("w-full gap-2 velai-lora-train-body"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
ui.label(
|
| 49 |
+
f"Dataset zip (PNG/JPG/WebP, optional .txt captions). Max {MAX_DATASET_ZIP_MB} MB."
|
| 50 |
+
).classes("text-xs text-gray-400")
|
| 51 |
+
|
| 52 |
+
self.upload = (
|
| 53 |
+
ui.upload(
|
| 54 |
+
label="Upload zip",
|
| 55 |
+
auto_upload=True,
|
| 56 |
+
on_upload=self.handle_upload,
|
| 57 |
+
multiple=False,
|
|
|
|
|
|
|
| 58 |
)
|
| 59 |
+
.props("accept=.zip,application/zip dense")
|
| 60 |
+
.classes("w-full velai-lora-upload")
|
| 61 |
+
)
|
| 62 |
+
self.file_list_label = ui.label("No zip uploaded yet").classes("text-xs text-gray-400")
|
| 63 |
+
|
| 64 |
+
with ui.grid(columns=2).classes("w-full gap-x-3 gap-y-2"):
|
| 65 |
+
self.lora_name = ui.input("LoRA name", value="My LoRA").classes("w-full").props("dense")
|
| 66 |
+
with ui.column().classes("w-full gap-0 justify-end"):
|
| 67 |
+
self.steps_label = ui.label("Training steps: 1000").classes("text-xs")
|
| 68 |
+
self.steps = ui.slider(min=1000, max=2000, step=1000, value=1000).props("w-full")
|
| 69 |
+
self.steps.on_value_change(
|
| 70 |
+
lambda e: setattr(self.steps_label, "text", f"Training steps: {int(e.value)}")
|
| 71 |
+
)
|
| 72 |
+
self.trigger_phrase = ui.input(
|
| 73 |
+
"Trigger phrase",
|
| 74 |
+
placeholder="e.g. ohwx person",
|
| 75 |
+
).classes("w-full").props("dense")
|
| 76 |
self.auto_captioning = ui.select(
|
| 77 |
{
|
| 78 |
"Off": "Off",
|
|
|
|
| 82 |
},
|
| 83 |
value="Off",
|
| 84 |
label="Auto captioning",
|
| 85 |
+
).classes("w-full").props("dense")
|
| 86 |
+
self.learning_rate = (
|
| 87 |
+
ui.number("Learning rate", value=0.0005, format="%.5f", min=0.00001, max=0.01)
|
| 88 |
+
.classes("w-full")
|
| 89 |
+
.props("dense")
|
| 90 |
+
)
|
| 91 |
+
self.resolution = ui.select(
|
| 92 |
+
{768: "768", 1024: "1024"},
|
| 93 |
+
value=768,
|
| 94 |
+
label="Resolution",
|
| 95 |
+
).classes("w-full").props("dense")
|
| 96 |
|
| 97 |
+
ui.label(
|
| 98 |
+
"Trigger phrase required if images have no .txt captions and auto-captioning is Off."
|
| 99 |
+
).classes("text-xs text-gray-400")
|
| 100 |
|
| 101 |
+
self.train_button = ui.button("Start Training", on_click=self.start_training).classes("w-full")
|
| 102 |
+
self.train_button.bind_enabled_from(self, "is_ready_to_train")
|
| 103 |
|
| 104 |
+
ui.label("Progress").classes("text-xs font-bold")
|
| 105 |
+
self.log = ui.log().classes("w-full h-28 velai-lora-log p-2 rounded text-xs font-mono")
|
| 106 |
+
|
| 107 |
+
self.result_container = ui.column().classes("w-full gap-1")
|
| 108 |
+
self.result_container.set_visibility(False)
|
| 109 |
+
with self.result_container:
|
| 110 |
+
ui.label("Training complete — LoRA saved to your library.").classes(
|
| 111 |
+
"text-sm font-bold text-green-400"
|
| 112 |
+
)
|
| 113 |
+
with ui.row().classes("w-full gap-2"):
|
| 114 |
+
self.download_lora_btn = ui.button("Download LoRA", icon="download").props("dense")
|
| 115 |
+
self.download_config_btn = ui.button("Download Config", icon="download").props("dense")
|
| 116 |
|
| 117 |
def _build_library_tab(self) -> None:
|
| 118 |
+
ui.label("LoRAs linked to this browser session").classes("text-sm text-gray-400 mb-2")
|
| 119 |
ui.label(
|
| 120 |
"They persist across reloads via your session cookie. Upload a .safetensors file or train a new one."
|
| 121 |
+
).classes("text-xs text-gray-400 mb-2")
|
| 122 |
|
| 123 |
self.lora_upload = (
|
| 124 |
ui.upload(
|
|
|
|
| 142 |
entries = list_loras(self._user_storage())
|
| 143 |
with self.library_container:
|
| 144 |
if not entries:
|
| 145 |
+
ui.label("No LoRAs yet. Train one or upload weights.").classes("text-sm text-gray-400")
|
| 146 |
return
|
| 147 |
for entry in entries:
|
| 148 |
+
with ui.card().classes("w-full velai-lora-entry"):
|
| 149 |
with ui.row().classes("w-full items-center justify-between"):
|
| 150 |
with ui.column().classes("gap-0"):
|
| 151 |
ui.label(entry.name).classes("font-bold")
|
| 152 |
meta = f"{entry.source} · {entry.created_at[:19]}"
|
| 153 |
if entry.trigger_phrase:
|
| 154 |
meta += f" · trigger: {entry.trigger_phrase}"
|
| 155 |
+
ui.label(meta).classes("text-xs text-gray-400")
|
| 156 |
with ui.row().classes("gap-1"):
|
| 157 |
ui.button(
|
| 158 |
icon="download",
|
|
|
|
| 306 |
original_name=lora_file.get("file_name") or "krea_lora.safetensors",
|
| 307 |
mime_type="application/octet-stream",
|
| 308 |
)
|
| 309 |
+
name = (self.lora_name.value or "My LoRA name").strip()
|
| 310 |
add_lora(
|
| 311 |
self._user_storage(),
|
| 312 |
name=name,
|