mlx-commander 0.3.19__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mlx_commander/__init__.py +12 -0
- mlx_commander/__main__.py +10 -0
- mlx_commander/bin/mac_picker.m +69 -0
- mlx_commander/cli.py +463 -0
- mlx_commander/converter.py +177 -0
- mlx_commander/exceptions.py +39 -0
- mlx_commander/formats.py +379 -0
- mlx_commander/gui_picker.py +246 -0
- mlx_commander/loader.py +1192 -0
- mlx_commander/lora/__init__.py +57 -0
- mlx_commander/lora/config.py +249 -0
- mlx_commander/lora/estimator.py +232 -0
- mlx_commander/lora/model_info.py +401 -0
- mlx_commander/lora/queue.py +212 -0
- mlx_commander/lora/runner.py +168 -0
- mlx_commander/lora/tracking.py +235 -0
- mlx_commander/mcp_server.py +308 -0
- mlx_commander/splitter.py +110 -0
- mlx_commander/terminal_spawner.py +212 -0
- mlx_commander/tui/__init__.py +7 -0
- mlx_commander/tui/app.py +1504 -0
- mlx_commander/tui/state.py +547 -0
- mlx_commander/tui/widgets.py +1985 -0
- mlx_commander/tui/wizard_fallback.py +631 -0
- mlx_commander-0.3.19.dist-info/METADATA +437 -0
- mlx_commander-0.3.19.dist-info/RECORD +29 -0
- mlx_commander-0.3.19.dist-info/WHEEL +4 -0
- mlx_commander-0.3.19.dist-info/entry_points.txt +2 -0
- mlx_commander-0.3.19.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mlx_commander - Hugging Face to MLX Dataset Converter
|
|
3
|
+
Convert Hugging Face datasets into Apple MLX fine-tuning formats with interactive TUI and CLI.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
# Set ncurses escape delay to 25ms (default is 1000ms) to ensure instantaneous
|
|
9
|
+
# exit/cancellation on ESC across all curses pickers and dialogs.
|
|
10
|
+
os.environ.setdefault("ESCDELAY", "25")
|
|
11
|
+
|
|
12
|
+
__version__ = "0.3.19"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#import <Cocoa/Cocoa.h>
|
|
2
|
+
|
|
3
|
+
int main(int argc, const char * argv[]) {
|
|
4
|
+
@autoreleasepool {
|
|
5
|
+
[NSApplication sharedApplication];
|
|
6
|
+
[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
|
|
7
|
+
[NSApp activateIgnoringOtherApps:YES];
|
|
8
|
+
|
|
9
|
+
NSOpenPanel *panel = [NSOpenPanel openPanel];
|
|
10
|
+
panel.canChooseFiles = YES;
|
|
11
|
+
panel.canChooseDirectories = YES;
|
|
12
|
+
panel.allowsMultipleSelection = YES;
|
|
13
|
+
panel.canCreateDirectories = YES;
|
|
14
|
+
panel.title = @"Select Dataset (File or Folder)";
|
|
15
|
+
panel.prompt = @"Select";
|
|
16
|
+
|
|
17
|
+
// Argument 1: mode ("both", "folder", "file")
|
|
18
|
+
if (argc > 1) {
|
|
19
|
+
NSString *mode = [NSString stringWithUTF8String:argv[1]];
|
|
20
|
+
if ([mode isEqualToString:@"folder"]) {
|
|
21
|
+
panel.canChooseFiles = NO;
|
|
22
|
+
panel.canChooseDirectories = YES;
|
|
23
|
+
panel.allowsMultipleSelection = NO;
|
|
24
|
+
panel.title = @"Select Destination Folder";
|
|
25
|
+
} else if ([mode isEqualToString:@"file"]) {
|
|
26
|
+
panel.canChooseFiles = YES;
|
|
27
|
+
panel.canChooseDirectories = NO;
|
|
28
|
+
panel.allowsMultipleSelection = YES;
|
|
29
|
+
panel.title = @"Select Dataset File(s)";
|
|
30
|
+
} else {
|
|
31
|
+
panel.canChooseFiles = YES;
|
|
32
|
+
panel.canChooseDirectories = YES;
|
|
33
|
+
panel.allowsMultipleSelection = YES;
|
|
34
|
+
panel.title = @"Select Dataset (File, Files, or Folder)";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Argument 2: initial directory
|
|
39
|
+
if (argc > 2) {
|
|
40
|
+
NSString *dirPath = [NSString stringWithUTF8String:argv[2]];
|
|
41
|
+
BOOL isDir = NO;
|
|
42
|
+
if ([[NSFileManager defaultManager] fileExistsAtPath:dirPath isDirectory:&isDir]) {
|
|
43
|
+
if (!isDir) {
|
|
44
|
+
dirPath = [dirPath stringByDeletingLastPathComponent];
|
|
45
|
+
}
|
|
46
|
+
panel.directoryURL = [NSURL fileURLWithPath:dirPath];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Argument 3: custom title
|
|
51
|
+
if (argc > 3) {
|
|
52
|
+
panel.title = [NSString stringWithUTF8String:argv[3]];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
[panel makeKeyAndOrderFront:nil];
|
|
56
|
+
[panel setLevel:NSFloatingWindowLevel];
|
|
57
|
+
|
|
58
|
+
NSModalResponse response = [panel runModal];
|
|
59
|
+
if (response == NSModalResponseOK) {
|
|
60
|
+
for (NSURL *selectedURL in [panel URLs]) {
|
|
61
|
+
if (selectedURL) {
|
|
62
|
+
printf("%s\n", [[selectedURL path] UTF8String]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
fflush(stdout);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
mlx_commander/cli.py
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command Line Interface (CLI) for mlx_commander converter.
|
|
3
|
+
Dispatches between full-screen Curses TUI, interactive terminal wizard,
|
|
4
|
+
and automated headless conversion.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
# Ensure ncurses escape delay is 25ms to make ESC instantaneous in all TUI pickers
|
|
13
|
+
os.environ.setdefault("ESCDELAY", "25")
|
|
14
|
+
|
|
15
|
+
import curses
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import List, Optional
|
|
18
|
+
|
|
19
|
+
from mlx_commander import __version__
|
|
20
|
+
from mlx_commander.converter import ConversionResult, convert_and_save
|
|
21
|
+
from mlx_commander.formats import (
|
|
22
|
+
ColumnMapping,
|
|
23
|
+
MLXFormat,
|
|
24
|
+
auto_detect_mapping,
|
|
25
|
+
validate_mapping,
|
|
26
|
+
)
|
|
27
|
+
from mlx_commander.exceptions import MissingDependencyError
|
|
28
|
+
from mlx_commander.loader import load_local_dataset
|
|
29
|
+
from mlx_commander.splitter import SplitConfig, generate_random_seed
|
|
30
|
+
from mlx_commander.tui.app import launch_tui
|
|
31
|
+
from mlx_commander.tui.wizard_fallback import run_interactive_wizard
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_mapping_arg(mapping_str: str) -> ColumnMapping:
|
|
35
|
+
"""Parse JSON or key=val,key=val string into ColumnMapping."""
|
|
36
|
+
clean = mapping_str.strip()
|
|
37
|
+
if clean.startswith("{"):
|
|
38
|
+
d = json.loads(clean)
|
|
39
|
+
return ColumnMapping(**d)
|
|
40
|
+
|
|
41
|
+
mapping = ColumnMapping()
|
|
42
|
+
for pair in clean.split(","):
|
|
43
|
+
if "=" in pair:
|
|
44
|
+
k, v = pair.split("=", 1)
|
|
45
|
+
k, v = k.strip(), v.strip()
|
|
46
|
+
if hasattr(mapping, k):
|
|
47
|
+
setattr(mapping, k, v)
|
|
48
|
+
elif k == "prompt":
|
|
49
|
+
mapping.prompt_col = v
|
|
50
|
+
elif k == "completion":
|
|
51
|
+
mapping.completion_col = v
|
|
52
|
+
elif k == "text":
|
|
53
|
+
mapping.text_col = v
|
|
54
|
+
elif k == "messages":
|
|
55
|
+
mapping.messages_col = v
|
|
56
|
+
elif k == "user":
|
|
57
|
+
mapping.user_col = v
|
|
58
|
+
elif k == "assistant":
|
|
59
|
+
mapping.assistant_col = v
|
|
60
|
+
elif k == "system":
|
|
61
|
+
mapping.system_col = v
|
|
62
|
+
elif k == "chosen":
|
|
63
|
+
mapping.chosen_col = v
|
|
64
|
+
elif k == "rejected":
|
|
65
|
+
mapping.rejected_col = v
|
|
66
|
+
return mapping
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parse_prefill_state(val: str) -> dict:
|
|
70
|
+
"""Parse prefill state from a JSON string or file path."""
|
|
71
|
+
clean = val.strip()
|
|
72
|
+
if clean.startswith("{"):
|
|
73
|
+
return json.loads(clean)
|
|
74
|
+
p = Path(clean).expanduser()
|
|
75
|
+
if p.exists() and p.is_file():
|
|
76
|
+
with open(p, "r", encoding="utf-8") as f:
|
|
77
|
+
return json.load(f)
|
|
78
|
+
try:
|
|
79
|
+
return json.loads(clean)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
raise ValueError(f"Could not parse --prefill-state as JSON or file path: {e}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
85
|
+
parser = argparse.ArgumentParser(
|
|
86
|
+
prog="mlx_commander",
|
|
87
|
+
description="Convert Hugging Face datasets into Apple MLX (mlx-lm) format with TUI or CLI.",
|
|
88
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
89
|
+
epilog="""
|
|
90
|
+
Examples:
|
|
91
|
+
# Launch interactive TUI wizard:
|
|
92
|
+
mlx_commander
|
|
93
|
+
|
|
94
|
+
# Launch line-by-line CLI wizard:
|
|
95
|
+
mlx_commander --no-tui
|
|
96
|
+
|
|
97
|
+
# Direct conversion from CLI:
|
|
98
|
+
mlx_commander -d ./my_hf_dataset -f prompt_completion -o ./mlx_out \
|
|
99
|
+
--prompt-col question --completion-col answer \
|
|
100
|
+
--train 80 --valid 10 --test 10 --seed 42
|
|
101
|
+
|
|
102
|
+
# Combine multiple dataset files with schema verification & re-splitting:
|
|
103
|
+
mlx_commander -d train.jsonl test.jsonl -f prompt_completion -o ./mlx_out \
|
|
104
|
+
--prompt-col question --completion-col answer
|
|
105
|
+
""",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
|
|
109
|
+
|
|
110
|
+
# Core conversion flags
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
"-d", "--dataset",
|
|
113
|
+
nargs="+",
|
|
114
|
+
type=str,
|
|
115
|
+
help="Path(s) to local Hugging Face dataset folder or data file(s) (.parquet, .arrow, .jsonl, .csv, .tsv, .sqlite, .tar). Multiple files will be verified for schema consistency and merged.",
|
|
116
|
+
)
|
|
117
|
+
parser.add_argument(
|
|
118
|
+
"-f", "--format",
|
|
119
|
+
type=str,
|
|
120
|
+
choices=["text", "chat", "prompt_completion", "dpo"],
|
|
121
|
+
help="Target MLX format: 'text', 'chat', 'prompt_completion', or 'dpo'.",
|
|
122
|
+
)
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
"-o", "--output",
|
|
125
|
+
type=str,
|
|
126
|
+
help="Destination directory where train.jsonl, valid.jsonl, test.jsonl will be saved (default: 'mlx_dataset' subfolder in same folder as source dataset).",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Split parameters
|
|
130
|
+
parser.add_argument("--train", type=float, default=None, help="Train split percentage (e.g. 80.0).")
|
|
131
|
+
parser.add_argument("--valid", type=float, default=None, help="Validation split percentage (e.g. 10.0).")
|
|
132
|
+
parser.add_argument("--test", type=float, default=None, help="Test split percentage (e.g. 10.0, or 0 to omit).")
|
|
133
|
+
parser.add_argument("--seed", type=int, default=None, help="Random seed for reproducible shuffling.")
|
|
134
|
+
parser.add_argument("--keep-splits", action="store_true", help="Preserve existing splits without re-splitting.")
|
|
135
|
+
|
|
136
|
+
# Column mapping flags
|
|
137
|
+
parser.add_argument("--mapping", type=str, help="Column mapping as JSON string or key=val,key=val.")
|
|
138
|
+
parser.add_argument("--text-col", type=str, help="Source column for 'text' format.")
|
|
139
|
+
parser.add_argument("--text-template", type=str, help="Template string for 'text' format (e.g. '{instruction}\\n{output}').")
|
|
140
|
+
parser.add_argument("--prompt-col", type=str, help="Source column for prompt / question.")
|
|
141
|
+
parser.add_argument("--completion-col", type=str, help="Source column for completion / answer.")
|
|
142
|
+
parser.add_argument("--messages-col", type=str, help="Source column containing chat messages list.")
|
|
143
|
+
parser.add_argument("--user-col", type=str, help="Source column for user turn in chat format.")
|
|
144
|
+
parser.add_argument("--assistant-col", type=str, help="Source column for assistant turn in chat format.")
|
|
145
|
+
parser.add_argument("--system-col", type=str, help="Source column for system prompt in chat format.")
|
|
146
|
+
parser.add_argument("--chosen-col", type=str, help="Source column for chosen response in DPO format.")
|
|
147
|
+
parser.add_argument("--rejected-col", type=str, help="Source column for rejected response in DPO format.")
|
|
148
|
+
|
|
149
|
+
# Agent & Hand-off flags
|
|
150
|
+
parser.add_argument(
|
|
151
|
+
"--manifest-file",
|
|
152
|
+
type=str,
|
|
153
|
+
default=None,
|
|
154
|
+
help="Custom file path where machine-readable mlx_manifest.json will be saved.",
|
|
155
|
+
)
|
|
156
|
+
parser.add_argument(
|
|
157
|
+
"--prefill-state",
|
|
158
|
+
type=str,
|
|
159
|
+
default=None,
|
|
160
|
+
help="Pre-populate TUI state from a JSON string or path to JSON file.",
|
|
161
|
+
)
|
|
162
|
+
parser.add_argument(
|
|
163
|
+
"--spawn-terminal",
|
|
164
|
+
action="store_true",
|
|
165
|
+
help="Spawn interactive TUI in an external macOS Terminal window (ideal for AI agents & subshells).",
|
|
166
|
+
)
|
|
167
|
+
parser.add_argument(
|
|
168
|
+
"--mcp",
|
|
169
|
+
action="store_true",
|
|
170
|
+
help="Launch Model Context Protocol (MCP) server over stdio for Claude Desktop, Cursor, etc.",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# UI mode flags
|
|
174
|
+
parser.add_argument(
|
|
175
|
+
"--commander", "--tui",
|
|
176
|
+
action="store_true",
|
|
177
|
+
dest="commander",
|
|
178
|
+
help="Launch full-screen persistent MLX Commander TUI dashboard (default in interactive terminal).",
|
|
179
|
+
)
|
|
180
|
+
parser.add_argument(
|
|
181
|
+
"--wizard", "--no-tui", "--cli",
|
|
182
|
+
action="store_true",
|
|
183
|
+
dest="wizard",
|
|
184
|
+
help="Run sequential step-by-step terminal wizard instead of persistent MLX Commander dashboard.",
|
|
185
|
+
)
|
|
186
|
+
parser.add_argument(
|
|
187
|
+
"--lora",
|
|
188
|
+
action="store_true",
|
|
189
|
+
help="Launch TUI directly in LoRA Fine-Tuning mode (Mode 2).",
|
|
190
|
+
)
|
|
191
|
+
parser.add_argument(
|
|
192
|
+
"--run-queue",
|
|
193
|
+
nargs="?",
|
|
194
|
+
const="mlx_runs",
|
|
195
|
+
type=str,
|
|
196
|
+
default=None,
|
|
197
|
+
help="Execute LoRA fine-tuning runs sequentially from queue directory (default: 'mlx_runs').",
|
|
198
|
+
)
|
|
199
|
+
parser.add_argument(
|
|
200
|
+
"--wandb-project",
|
|
201
|
+
type=str,
|
|
202
|
+
default="mlx-commander",
|
|
203
|
+
help="Weights & Biases project name for experiment tracking (default: 'mlx-commander').",
|
|
204
|
+
)
|
|
205
|
+
parser.add_argument(
|
|
206
|
+
"--no-wandb",
|
|
207
|
+
action="store_true",
|
|
208
|
+
help="Disable Weights & Biases experiment logging even if wandb is installed and logged in.",
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return parser
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def run_direct_conversion(args: argparse.Namespace) -> ConversionResult:
|
|
215
|
+
"""Perform headless conversion using command-line arguments."""
|
|
216
|
+
dataset = load_local_dataset(args.dataset)
|
|
217
|
+
target_format = MLXFormat(args.format.lower())
|
|
218
|
+
|
|
219
|
+
# Build mapping
|
|
220
|
+
if args.mapping:
|
|
221
|
+
mapping = parse_mapping_arg(args.mapping)
|
|
222
|
+
else:
|
|
223
|
+
mapping = auto_detect_mapping(target_format, dataset.columns)
|
|
224
|
+
if args.text_col:
|
|
225
|
+
mapping.text_col = args.text_col
|
|
226
|
+
if args.text_template:
|
|
227
|
+
mapping.text_template = args.text_template
|
|
228
|
+
if args.prompt_col:
|
|
229
|
+
mapping.prompt_col = args.prompt_col
|
|
230
|
+
if args.completion_col:
|
|
231
|
+
mapping.completion_col = args.completion_col
|
|
232
|
+
if args.messages_col:
|
|
233
|
+
mapping.messages_col = args.messages_col
|
|
234
|
+
if args.user_col:
|
|
235
|
+
mapping.user_col = args.user_col
|
|
236
|
+
if args.assistant_col:
|
|
237
|
+
mapping.assistant_col = args.assistant_col
|
|
238
|
+
if args.system_col:
|
|
239
|
+
mapping.system_col = args.system_col
|
|
240
|
+
if args.chosen_col:
|
|
241
|
+
mapping.chosen_col = args.chosen_col
|
|
242
|
+
if args.rejected_col:
|
|
243
|
+
mapping.rejected_col = args.rejected_col
|
|
244
|
+
|
|
245
|
+
# Validate mapping
|
|
246
|
+
errs = validate_mapping(target_format, mapping, dataset.columns)
|
|
247
|
+
if errs:
|
|
248
|
+
raise ValueError("Mapping error: " + "; ".join(errs))
|
|
249
|
+
|
|
250
|
+
# Split config
|
|
251
|
+
split_config = None
|
|
252
|
+
if not (args.keep_splits and dataset.is_split):
|
|
253
|
+
train_p = args.train if args.train is not None else 80.0
|
|
254
|
+
valid_p = args.valid if args.valid is not None else 10.0
|
|
255
|
+
test_p = args.test if args.test is not None else (100.0 - train_p - valid_p)
|
|
256
|
+
seed = args.seed if args.seed is not None else generate_random_seed()
|
|
257
|
+
split_config = SplitConfig(train_pct=train_p, valid_pct=valid_p, test_pct=test_p, seed=seed)
|
|
258
|
+
v_errs = split_config.validate()
|
|
259
|
+
if v_errs:
|
|
260
|
+
raise ValueError("Split error: " + "; ".join(v_errs))
|
|
261
|
+
|
|
262
|
+
out_dir = args.output or str(dataset.default_output_dir)
|
|
263
|
+
return convert_and_save(
|
|
264
|
+
dataset=dataset,
|
|
265
|
+
format_type=target_format,
|
|
266
|
+
mapping=mapping,
|
|
267
|
+
output_dir=out_dir,
|
|
268
|
+
split_config=split_config,
|
|
269
|
+
use_existing_splits=args.keep_splits,
|
|
270
|
+
manifest_file=getattr(args, "manifest_file", None),
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def is_interactive_tty() -> bool:
|
|
275
|
+
"""Check if stdout and stdin are interactive TTYs with adequate terminfo."""
|
|
276
|
+
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
277
|
+
return False
|
|
278
|
+
term = os.environ.get("TERM", "")
|
|
279
|
+
if not term or term == "dumb":
|
|
280
|
+
return False
|
|
281
|
+
return True
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
285
|
+
parser = build_parser()
|
|
286
|
+
args = parser.parse_args(argv)
|
|
287
|
+
|
|
288
|
+
if args.mcp:
|
|
289
|
+
from mlx_commander.mcp_server import run_mcp_server
|
|
290
|
+
return run_mcp_server()
|
|
291
|
+
|
|
292
|
+
# Case 0: Sequential LoRA queue execution
|
|
293
|
+
if args.run_queue is not None:
|
|
294
|
+
from mlx_commander.lora import run_lora_queue
|
|
295
|
+
q_dir = Path(args.run_queue)
|
|
296
|
+
ok = run_lora_queue(
|
|
297
|
+
q_dir,
|
|
298
|
+
wandb_project=args.wandb_project,
|
|
299
|
+
enable_wandb=not args.no_wandb,
|
|
300
|
+
)
|
|
301
|
+
return 0 if ok else 1
|
|
302
|
+
|
|
303
|
+
# Normalize dataset path argument
|
|
304
|
+
dataset_input = None
|
|
305
|
+
if args.dataset:
|
|
306
|
+
if isinstance(args.dataset, list):
|
|
307
|
+
dataset_input = "\n".join(args.dataset) if len(args.dataset) > 1 else args.dataset[0]
|
|
308
|
+
else:
|
|
309
|
+
dataset_input = args.dataset
|
|
310
|
+
|
|
311
|
+
# Case 1: Direct Headless Run (dataset and format specified, without explicit UI flags)
|
|
312
|
+
if args.dataset and args.format and not args.wizard and not args.commander and not args.spawn_terminal:
|
|
313
|
+
try:
|
|
314
|
+
result = run_direct_conversion(args)
|
|
315
|
+
src_desc = f"{len(args.dataset)} files (merged)" if isinstance(args.dataset, list) and len(args.dataset) > 1 else (args.dataset[0] if isinstance(args.dataset, list) else str(args.dataset))
|
|
316
|
+
print(f"[OK] Successfully converted {src_desc} to {args.format} format in {result.output_dir}")
|
|
317
|
+
for s_name, path in result.output_files.items():
|
|
318
|
+
cnt = result.record_counts.get(s_name, 0)
|
|
319
|
+
print(f" • {path.name}: {cnt:,} records")
|
|
320
|
+
if result.manifest_path:
|
|
321
|
+
print(f" • Manifest: {result.manifest_path}")
|
|
322
|
+
print(f"\nMLX Fine-tuning command:\n{result.generate_mlx_lora_command()}\n")
|
|
323
|
+
return 0
|
|
324
|
+
except KeyboardInterrupt:
|
|
325
|
+
print("\nOperation cancelled.")
|
|
326
|
+
return 130
|
|
327
|
+
except MissingDependencyError as e:
|
|
328
|
+
print(f"\nMissing Dependency Error:\n{e.format_name} format requires package '{e.package_name}'.", file=sys.stderr)
|
|
329
|
+
print(f"\nTo install, run:\n {e.install_command}", file=sys.stderr)
|
|
330
|
+
print(f"or install optional extra:\n {e.pip_extra}\n", file=sys.stderr)
|
|
331
|
+
return 1
|
|
332
|
+
except Exception as e:
|
|
333
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
334
|
+
return 1
|
|
335
|
+
|
|
336
|
+
# Case 2: Sequential wizard explicitly requested via --wizard / --no-tui / --cli
|
|
337
|
+
if args.wizard:
|
|
338
|
+
try:
|
|
339
|
+
run_interactive_wizard(
|
|
340
|
+
dataset_path=dataset_input,
|
|
341
|
+
format_arg=args.format,
|
|
342
|
+
output_dir_arg=args.output,
|
|
343
|
+
train_pct_arg=args.train,
|
|
344
|
+
valid_pct_arg=args.valid,
|
|
345
|
+
test_pct_arg=args.test,
|
|
346
|
+
seed_arg=args.seed,
|
|
347
|
+
)
|
|
348
|
+
return 0
|
|
349
|
+
except (KeyboardInterrupt, EOFError):
|
|
350
|
+
print("\nOperation cancelled.")
|
|
351
|
+
return 130
|
|
352
|
+
except MissingDependencyError as e:
|
|
353
|
+
print(f"\nMissing Dependency Error:\n{e.format_name} format requires package '{e.package_name}'.", file=sys.stderr)
|
|
354
|
+
print(f"\nTo install, run:\n {e.install_command}", file=sys.stderr)
|
|
355
|
+
print(f"or install optional extra:\n {e.pip_extra}\n", file=sys.stderr)
|
|
356
|
+
return 1
|
|
357
|
+
except Exception as e:
|
|
358
|
+
print(f"\nError: {e}", file=sys.stderr)
|
|
359
|
+
return 1
|
|
360
|
+
|
|
361
|
+
# Case 3: Interactive TUI Dashboard (with optional prefill and Terminal Spawner)
|
|
362
|
+
prefill_dict: Dict[str, Any] = {}
|
|
363
|
+
if args.prefill_state:
|
|
364
|
+
try:
|
|
365
|
+
prefill_dict.update(parse_prefill_state(args.prefill_state))
|
|
366
|
+
except Exception as e:
|
|
367
|
+
print(f"Error parsing --prefill-state: {e}", file=sys.stderr)
|
|
368
|
+
return 1
|
|
369
|
+
|
|
370
|
+
if args.format:
|
|
371
|
+
prefill_dict["format"] = args.format
|
|
372
|
+
if args.prompt_col:
|
|
373
|
+
prefill_dict["prompt_col"] = args.prompt_col
|
|
374
|
+
if args.completion_col:
|
|
375
|
+
prefill_dict["completion_col"] = args.completion_col
|
|
376
|
+
if args.text_col:
|
|
377
|
+
prefill_dict["text_col"] = args.text_col
|
|
378
|
+
if args.text_template:
|
|
379
|
+
prefill_dict["text_template"] = args.text_template
|
|
380
|
+
if args.messages_col:
|
|
381
|
+
prefill_dict["messages_col"] = args.messages_col
|
|
382
|
+
if args.user_col:
|
|
383
|
+
prefill_dict["user_col"] = args.user_col
|
|
384
|
+
if args.assistant_col:
|
|
385
|
+
prefill_dict["assistant_col"] = args.assistant_col
|
|
386
|
+
if args.system_col:
|
|
387
|
+
prefill_dict["system_col"] = args.system_col
|
|
388
|
+
if args.chosen_col:
|
|
389
|
+
prefill_dict["chosen_col"] = args.chosen_col
|
|
390
|
+
if args.rejected_col:
|
|
391
|
+
prefill_dict["rejected_col"] = args.rejected_col
|
|
392
|
+
if args.train is not None:
|
|
393
|
+
prefill_dict["train"] = args.train
|
|
394
|
+
if args.valid is not None:
|
|
395
|
+
prefill_dict["valid"] = args.valid
|
|
396
|
+
if args.test is not None:
|
|
397
|
+
prefill_dict["test"] = args.test
|
|
398
|
+
if args.seed is not None:
|
|
399
|
+
prefill_dict["seed"] = args.seed
|
|
400
|
+
if args.output:
|
|
401
|
+
prefill_dict["output"] = args.output
|
|
402
|
+
if dataset_input:
|
|
403
|
+
prefill_dict["dataset"] = dataset_input
|
|
404
|
+
if args.lora:
|
|
405
|
+
prefill_dict["tab"] = 1
|
|
406
|
+
prefill_dict["active_tab"] = 1
|
|
407
|
+
if args.wandb_project:
|
|
408
|
+
prefill_dict["wandb_project"] = args.wandb_project
|
|
409
|
+
if args.no_wandb:
|
|
410
|
+
prefill_dict["wandb_enabled"] = False
|
|
411
|
+
|
|
412
|
+
# Check if external terminal window should be spawned
|
|
413
|
+
from mlx_commander.terminal_spawner import is_macos, spawn_terminal_tui
|
|
414
|
+
should_spawn = args.spawn_terminal or (not is_interactive_tty() and is_macos())
|
|
415
|
+
|
|
416
|
+
if should_spawn:
|
|
417
|
+
raw_args = list(sys.argv[1:] if argv is None else argv)
|
|
418
|
+
return spawn_terminal_tui(raw_args, manifest_path=args.manifest_file)
|
|
419
|
+
|
|
420
|
+
if not is_interactive_tty():
|
|
421
|
+
print(
|
|
422
|
+
"Error: No interactive terminal (TTY) detected.\n"
|
|
423
|
+
"On macOS, use --spawn-terminal to launch in Terminal.app, or specify --format for headless conversion.",
|
|
424
|
+
file=sys.stderr,
|
|
425
|
+
)
|
|
426
|
+
return 1
|
|
427
|
+
|
|
428
|
+
try:
|
|
429
|
+
result = launch_tui(default_dataset_path=dataset_input, prefill=prefill_dict)
|
|
430
|
+
if result is not None:
|
|
431
|
+
if args.manifest_file:
|
|
432
|
+
result.save_manifest(args.manifest_file)
|
|
433
|
+
print(f"\n[OK] Successfully converted dataset to {result.format_type.value} format in {result.output_dir}")
|
|
434
|
+
if result.manifest_path:
|
|
435
|
+
print(f" • Manifest: {result.manifest_path}")
|
|
436
|
+
return 0
|
|
437
|
+
else:
|
|
438
|
+
if args.manifest_file:
|
|
439
|
+
try:
|
|
440
|
+
Path(args.manifest_file).parent.mkdir(parents=True, exist_ok=True)
|
|
441
|
+
with open(args.manifest_file, "w", encoding="utf-8") as f:
|
|
442
|
+
json.dump({"status": "cancelled"}, f, indent=2)
|
|
443
|
+
except Exception:
|
|
444
|
+
pass
|
|
445
|
+
print("\nOperation cancelled.")
|
|
446
|
+
return 130
|
|
447
|
+
except (KeyboardInterrupt, EOFError):
|
|
448
|
+
print("\nOperation cancelled.")
|
|
449
|
+
return 130
|
|
450
|
+
except curses.error as e:
|
|
451
|
+
print(f"\nTerminal error: {e}", file=sys.stderr)
|
|
452
|
+
return 1
|
|
453
|
+
except Exception as e:
|
|
454
|
+
print(f"\nError: {e}", file=sys.stderr)
|
|
455
|
+
return 1
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
if __name__ == "__main__":
|
|
459
|
+
try:
|
|
460
|
+
sys.exit(main())
|
|
461
|
+
except KeyboardInterrupt:
|
|
462
|
+
print("\nOperation cancelled.")
|
|
463
|
+
sys.exit(130)
|