Skip to content
Closed
Show file tree
Hide file tree
Changes from 62 commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
fad971a
Move trainer cleanup under finally
O-J1 Jul 4, 2025
13efc33
Tweak UI layout and labels
O-J1 Jul 4, 2025
e5026c8
Centralise tensorboard management + mutually exclusive toggles
O-J1 Jul 4, 2025
84c9f89
Create tensorboard_util that both BaseTrainer and TrainUI can import
O-J1 Jul 4, 2025
6c7a7bc
Move to option_kv to reduce clutter and make UI logic easier.
O-J1 Jul 4, 2025
aef07ad
Made the default value always on
O-J1 Jul 4, 2025
7f7796c
Potential fix for cloud always-on conflict.
O-J1 Jul 12, 2025
e84e9e4
Merge branch 'master' into tensorboard-on-tweaks
O-J1 Sep 2, 2025
15256d8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 2, 2025
1a8ef47
Merge remote-tracking branch 'origin/master' into tensorboard-on-tweaks
O-J1 Oct 4, 2025
272e8d4
Adds initial drag-n-drop fuctionality
O-J1 Oct 12, 2025
15bad8b
Add dnd to conceptsTab and small cleanup
O-J1 Oct 13, 2025
3ca7973
Add tkinterdnd2 to requirements
O-J1 Oct 13, 2025
edd7001
Allow multi dnd for concepts and cleanup validation further + move to…
O-J1 Oct 13, 2025
c216f19
Adds XL Lora output correction to saving + bugfix
O-J1 Oct 13, 2025
e2c728f
Merge remote-tracking branch 'origin/master' into tensorboard-on-tweaks
O-J1 Oct 14, 2025
f759a8f
Move the migration to migration 8
O-J1 Oct 14, 2025
784fb3d
Attempt at fixing debounced workspace bug + cleanup
O-J1 Oct 14, 2025
1a442e5
Fix returning the migrated data
O-J1 Oct 14, 2025
9fd33a2
Merge oj1/drag-n-drop-file-explorer into tensorboard-on-tweaks
O-J1 Oct 14, 2025
10caaf3
Remove debug logging and add is_output for dir_entry as well.
O-J1 Oct 14, 2025
a65350a
Account for cloud tunnel and add whitespace trim for dir_entry on las…
O-J1 Oct 14, 2025
9c69bbd
Fix(?) cloud regression with tensorboard
O-J1 Oct 16, 2025
9c9d620
Merge remote-tracking branch 'origin/master' into tensorboard-on-tweaks
O-J1 Oct 16, 2025
e8c30d1
Input validation logic
O-J1 Nov 8, 2025
e5f94e7
Merge remote-tracking branch 'origin/master' into tensorboard-on-tweaks
O-J1 Nov 9, 2025
b660943
Trimming the fat from components.py and remove legacy file_entry/dir_…
O-J1 Nov 9, 2025
60407ad
Further trim fat from components.py and remove model_output_entry
O-J1 Nov 9, 2025
079d1a5
More trimming, testing as I go along
O-J1 Nov 9, 2025
04963d5
Even more fat trimming.
O-J1 Nov 9, 2025
57a5933
Trimming the fat from validation.py
O-J1 Nov 9, 2025
2d79fe0
Remove dead function and remove CLI save bugfix, out of scope
O-J1 Nov 9, 2025
4778222
- Make tensorboard use the model output name
O-J1 Nov 10, 2025
53347ac
Merge branch 'Nerogar:master' into tensorboard-on-tweaks
O-J1 Nov 10, 2025
33fc76f
Add logic to validate on load to avoid edge cases
O-J1 Nov 10, 2025
169c031
Edgecase fix
O-J1 Nov 10, 2025
5936740
Utilise existing debouncTimer
O-J1 Nov 10, 2025
e2d2f64
Merge branch 'master' into tensorboard-on-tweaks
O-J1 Nov 13, 2025
134957f
Remove unnecessary blank line in model saver
dxqb Nov 13, 2025
7d4829f
Merge branch 'Nerogar:master' into tensorboard-on-tweaks
O-J1 Nov 19, 2025
12d634c
Add missing req
O-J1 Nov 20, 2025
dc900cb
Make all the enums uppercase
O-J1 Nov 20, 2025
5a06a00
Remove no longer used code in BaseTrainer
O-J1 Nov 20, 2025
74b4f2e
- Align run name inconsistency
O-J1 Nov 20, 2025
3ba4cd8
Move all relevant code under finally block
O-J1 Nov 20, 2025
3cd211e
Attempt to trace the state of cloud enabled and tcp tunnel as well.
O-J1 Nov 20, 2025
d6146e1
Fix scalene and prevent each input binding event handlers to root. Sa…
O-J1 Nov 20, 2025
b12b20d
Update scalene
O-J1 Nov 21, 2025
9a15f6d
Introduce deferred tab loading.
O-J1 Nov 21, 2025
a5e1e8f
Merge remote-tracking branch 'origin/master' into tensorboard-on-tweaks
O-J1 Nov 21, 2025
87cb6b8
Fix syntax for friendlywords version in requirements
dxqb Nov 29, 2025
339d376
Fix StrEnum and tweak deprecated warning message
O-J1 Dec 4, 2025
1837050
Merge branch 'tensorboard-on-tweaks' of https://github.com/O-J1/OneTr…
O-J1 Dec 4, 2025
d01ae67
merge
dxqb Dec 16, 2025
b7de8ad
merge fix
dxqb Dec 16, 2025
69aca0c
Fix accidental row reuse
O-J1 Jan 1, 2026
0f9cb0d
Make model output dest dragNdrop reject invalid filepaths i.e txt
O-J1 Jan 2, 2026
2a128cf
Modify scalene env var name to OT_PROFILE
O-J1 Jan 2, 2026
1e6923c
Remove redundant checks
O-J1 Jan 2, 2026
66653dd
Tweak gitignore.
O-J1 Jan 2, 2026
30b156d
Fix CLI tensorboard oversight
O-J1 Jan 2, 2026
d8ebd21
Make saves and backups use run name
O-J1 Jan 2, 2026
ff8edb9
Attempts at debugging Xubuntu issues with dnd
O-J1 Jan 11, 2026
b2b44f2
Make the placeholder text implementation better
O-J1 Jan 11, 2026
ab07e7b
Bump tensorboard
O-J1 Jan 17, 2026
1fe3257
Remove debug, bump av to allow fresh install
O-J1 Jan 18, 2026
7e09675
Destroy fig and canvas when window is closed
O-J1 Jan 18, 2026
2e93765
Allow HF repo style inputs to skip validation
O-J1 Jan 18, 2026
6105204
Add two HF rules I wasnt aware of.
O-J1 Jan 18, 2026
2e60c7b
Allow HF HTTP urls to also skip validation
O-J1 Jan 18, 2026
71cde89
Experimental test to see if FigureCanvasTkAgg is the issue
O-J1 Jan 18, 2026
c260031
Revert "Experimental test to see if FigureCanvasTkAgg is the issue"
O-J1 Jan 18, 2026
f434a76
Move statistics to default Agg backend instead of interactive one
O-J1 Jan 18, 2026
a145528
Skip scanning for invalid concepts
O-J1 Jan 18, 2026
79498d8
Disable DnD on linux due to unstable behaviour
O-J1 Feb 4, 2026
935dc0e
Add undo/redo. Remove revert behaviour. Prevent training startining w…
O-J1 Feb 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
debug*.ipynb
debug*.py
/debug*
profile.json
profile.html
stacks.txt

# user data
/workspace*
Expand All @@ -16,7 +19,7 @@ debug*.py
/training_user_settings
/external
/update.var
config.json
config*.json
secrets.json
*.zip

Expand Down
25 changes: 0 additions & 25 deletions modules/trainer/BaseTrainer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import os
import subprocess
import sys
from abc import ABCMeta, abstractmethod

from modules.model.BaseModel import BaseModel
Expand All @@ -23,8 +20,6 @@ class BaseTrainer(
metaclass=ABCMeta,
):

tensorboard_subprocess: subprocess.Popen

def __init__(self, config: TrainConfig, callbacks: TrainCallbacks, commands: TrainCommands):
super().__init__()
self.config = config
Expand Down Expand Up @@ -80,23 +75,3 @@ def create_model_sampler(self, model: BaseModel) -> BaseModelSampler:
self.config.model_type,
self.config.training_method
)
def _start_tensorboard(self):
tensorboard_executable = os.path.join(os.path.dirname(sys.executable), "tensorboard")
tensorboard_log_dir = os.path.join(self.config.workspace_dir, "tensorboard")

tensorboard_args = [
tensorboard_executable,
"--logdir",
tensorboard_log_dir,
"--port",
str(self.config.tensorboard_port),
"--samples_per_plugin=images=100,scalars=10000",
]

if self.config.tensorboard_expose:
tensorboard_args.append("--bind_all")

self.tensorboard_subprocess = subprocess.Popen(tensorboard_args)

def _stop_tensorboard(self):
self.tensorboard_subprocess.kill()
8 changes: 4 additions & 4 deletions modules/trainer/CloudTrainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from modules.util.config.TrainConfig import TrainConfig
from modules.util.enum.CloudAction import CloudAction
from modules.util.enum.CloudType import CloudType
from modules.util.tensorboard_util import get_tensorboard_manager


class CloudTrainer(BaseTrainer):
Expand All @@ -29,8 +30,8 @@ def __init__(self, config: TrainConfig, callbacks: TrainCallbacks, commands: Tra

tensorboard_log_dir = os.path.join(config.workspace_dir, "tensorboard")
os.makedirs(Path(tensorboard_log_dir).absolute(), exist_ok=True)
if config.tensorboard and not config.cloud.tensorboard_tunnel and not config.tensorboard_always_on:
super()._start_tensorboard()

get_tensorboard_manager().handle_training_start(config)

match config.cloud.type:
case CloudType.RUNPOD:
Expand Down Expand Up @@ -113,8 +114,7 @@ def train(self):

def end(self):
try:
if self.config.tensorboard and not self.config.cloud.tensorboard_tunnel and not self.config.tensorboard_always_on:
super()._stop_tensorboard()
get_tensorboard_manager().handle_training_end(self.config)

if self.config.cloud.delete_workspace and not self.error_caught and not self.commands.get_stop_command():
self.callbacks.on_update_status("Deleting remote workspace")
Expand Down
21 changes: 10 additions & 11 deletions modules/trainer/GenericTrainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from modules.util.enum.TimeUnit import TimeUnit
from modules.util.enum.TrainingMethod import TrainingMethod
from modules.util.profiling_util import TorchMemoryRecorder, TorchProfiler
from modules.util.tensorboard_util import get_tensorboard_manager
from modules.util.time_util import get_string_timestamp
from modules.util.torch_util import torch_gc
from modules.util.TrainProgress import TrainProgress
Expand Down Expand Up @@ -70,9 +71,9 @@ def __init__(self, config: TrainConfig, callbacks: TrainCallbacks, commands: Tra
if multi.is_master():
tensorboard_log_dir = os.path.join(config.workspace_dir, "tensorboard")
os.makedirs(Path(tensorboard_log_dir).absolute(), exist_ok=True)
self.tensorboard = SummaryWriter(os.path.join(tensorboard_log_dir, f"{config.save_filename_prefix}{get_string_timestamp()}"))
if config.tensorboard and not config.tensorboard_always_on:
Comment thread
O-J1 marked this conversation as resolved.
Resolved
super()._start_tensorboard()
self.tensorboard = SummaryWriter(os.path.join(tensorboard_log_dir, f"{config.get_run_name()}_{get_string_timestamp()}"))

get_tensorboard_manager().handle_training_start(config)

self.model = None
self.one_step_trained = False
Expand Down Expand Up @@ -164,7 +165,7 @@ def start(self):
def __save_config_to_workspace(self):
path = path_util.canonical_join(self.config.workspace_dir, "config")
os.makedirs(Path(path).absolute(), exist_ok=True)
path = path_util.canonical_join(path, f"{self.config.save_filename_prefix}{get_string_timestamp()}.json")
path = path_util.canonical_join(path, f"{self.config.get_run_name()}_{get_string_timestamp()}.json")
with open(path, "w") as f:
json.dump(self.config.to_pack_dict(secrets=False), f, indent=4)

Expand Down Expand Up @@ -234,7 +235,7 @@ def __sample_loop(

sample_path = os.path.join(
sample_dir,
f"{self.config.save_filename_prefix}{get_string_timestamp()}-training-sample-{train_progress.filename_string()}"
f"{self.config.get_run_name()}_{get_string_timestamp()}-training-sample-{train_progress.filename_string()}"
)

def on_sample_default(sampler_output: ModelSamplerOutput):
Expand Down Expand Up @@ -431,7 +432,7 @@ def __backup(self, train_progress: TrainProgress, print_msg: bool = True, print_

self.callbacks.on_update_status("Creating backup")

backup_name = f"{get_string_timestamp()}-backup-{train_progress.filename_string()}"
backup_name = f"{get_string_timestamp()}-{self.config.get_run_name()}-backup-{train_progress.filename_string()}"
backup_path = os.path.join(self.config.workspace_dir, "backup", backup_name)

# Special case for schedule-free optimizers.
Expand Down Expand Up @@ -481,7 +482,7 @@ def __save(self, train_progress: TrainProgress, print_msg: bool = True, print_cb
save_path = os.path.join(
self.config.workspace_dir,
"save",
f"{self.config.save_filename_prefix}{get_string_timestamp()}-save-{train_progress.filename_string()}{self.config.output_model_format.file_extension()}"
f"{self.config.save_filename_prefix}{get_string_timestamp()}-{self.config.get_run_name()}-save-{train_progress.filename_string()}{self.config.output_model_format.file_extension()}"
)
if print_msg:
print_cb("Saving " + save_path)
Expand Down Expand Up @@ -851,7 +852,7 @@ def end(self):
if os.path.isdir(self.config.output_model_destination) and self.config.output_model_format.is_single_file():
save_path = os.path.join(
self.config.output_model_destination,
f"{self.config.save_filename_prefix}{get_string_timestamp()}{self.config.output_model_format.file_extension()}"
f"{self.config.get_run_name()}_{get_string_timestamp()}{self.config.output_model_format.file_extension()}"
)
else:
save_path = self.config.output_model_destination
Expand All @@ -870,9 +871,7 @@ def end(self):

if multi.is_master():
self.tensorboard.close()

if self.config.tensorboard and not self.config.tensorboard_always_on:
super()._stop_tensorboard()
get_tensorboard_manager().handle_training_end(self.config)

for handle in self.grad_hook_handles:
handle.remove()
2 changes: 1 addition & 1 deletion modules/ui/AdditionalEmbeddingsTab.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def __init__(self, master, element, i, open_command, remove_command, clone_comma
# embedding model names
components.label(top_frame, 0, 2, "base embedding:",
tooltip="The base embedding to train on. Leave empty to create a new embedding")
components.file_entry(
components.path_entry(
top_frame, 0, 3, self.ui_state, "model_name",
path_modifier=lambda x: Path(x).parent.absolute() if x.endswith(".json") else x
)
Expand Down
2 changes: 1 addition & 1 deletion modules/ui/CloudTab.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def __init__(self, master, train_config: TrainConfig, ui_state: UIState, parent)

components.label(self.frame, 7, 0, "SSH keyfile path",
tooltip="Absolute path to the private key file used for SSH connections. Leave empty to rely on your system SSH configuration.")
components.file_entry(self.frame, 7, 1, self.ui_state, "secrets.cloud.key_file") #TODO Replace with path_entry in a future PR
components.path_entry(self.frame, 7, 1, self.ui_state, "secrets.cloud.key_file")

components.label(self.frame, 8, 0, "SSH password",
tooltip="SSH password for password-based authentication. If you try to use native SCP requires sshpass to be installed. Leave empty to use key-based authentication.")
Expand Down
37 changes: 32 additions & 5 deletions modules/ui/ConceptTab.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,14 @@ def __init__(self, master, concept, i, open_command, remove_command, clone_comma

self.grid_rowconfigure(1, weight=1)

# image
# image - start with placeholder, load actual image async
self._placeholder_image = self.__get_placeholder_image()
self.image = ctk.CTkImage(
light_image=self.__get_preview_image(),
light_image=self._placeholder_image,
size=(150, 150)
)
image_label = ctk.CTkLabel(master=self, text="", image=self.image, height=150, width=150)
image_label.grid(row=0, column=0)
self.image_label = ctk.CTkLabel(master=self, text="", image=self.image, height=150, width=150)
self.image_label.grid(row=0, column=0)

# name
self.name_label = components.label(self, 1, 0, self.__get_display_name(), pad=5, wraplength=140)
Expand Down Expand Up @@ -224,15 +225,41 @@ def __init__(self, master, concept, i, open_command, remove_command, clone_comma
)
enabled_switch.place(x=110, y=0)

image_label.bind(
self.image_label.bind(
"<Button-1>",
lambda event: open_command(self.i, (self.ui_state, self.image_ui_state, self.text_ui_state))
)

# defer actual image
self.after(50, self.__load_preview_image_deferred)

def __randomize_seed(self, concept: ConceptConfig):
concept.seed = ConceptConfig.default_values().seed
return concept

_placeholder_image_cache = None

def __get_placeholder_image(self):
if ConceptWidget._placeholder_image_cache:
return ConceptWidget._placeholder_image_cache

try:
image = load_image("resources/icons/icon.png", convert_mode="RGBA")
size = min(image.width, image.height)
image = image.crop(((image.width - size) // 2, (image.height - size) // 2, (image.width + size) // 2, (image.height + size) // 2))
except (OSError):
image = Image.new("RGBA", (150, 150), (200, 200, 200, 255))

ConceptWidget._placeholder_image_cache = image.resize((150, 150), Image.Resampling.BILINEAR)
return ConceptWidget._placeholder_image_cache

def __load_preview_image_deferred(self):
try:
actual_image = self.__get_preview_image()
self.image.configure(light_image=actual_image)
except Exception:
pass # Keep placeholder on error

def __get_display_name(self):
if self.concept.name:
return self.concept.name
Expand Down
4 changes: 2 additions & 2 deletions modules/ui/ConceptWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,14 @@ def __general_tab(self, master, concept: ConceptConfig):
# path
components.label(frame, 3, 0, "Path",
tooltip="Path where the training data is located")
components.dir_entry(frame, 3, 1, self.ui_state, "path")
components.path_entry(frame, 3, 1, self.ui_state, "path", path_type="directory")
components.button(frame, 3, 2, text="download now", command=self.__download_dataset_threaded,
tooltip="Download dataset from Huggingface now, for the purpose of previewing and statistics. Otherwise, it will be downloaded when you start training. Path must be a Huggingface repository.")

# prompt source
components.label(frame, 4, 0, "Prompt Source",
tooltip="The source for prompts used during training. When selecting \"From single text file\", select a text file that contains a list of prompts")
prompt_path_entry = components.file_entry(frame, 4, 2, self.text_ui_state, "prompt_path")
prompt_path_entry = components.path_entry(frame, 4, 2, self.text_ui_state, "prompt_path")

def set_prompt_path_entry_enabled(option: str):
if option == 'concept':
Expand Down
Loading