1
from __future__ import annotations3
import argparse4
import hashlib5
import inspect6
import json7
import random8
import shutil9
import sys10
import time11
from collections import defaultdict12
from pathlib import Path13
from typing import Any, Dict, List, Sequence, Tuple15
from ai_guitar_pedal.storage_redirect import (16
E_ROOT,17
apply_env_defaults,18
assert_under_root,19
ensure_storage_tree,20
resolve_e_root,21
)24
def early_arg_value(argv: Sequence[str], name: str) -> str | None:25
prefix = f"{name}="26
for idx, item in enumerate(argv):27
if item == name and idx + 1 < len(argv):28
return argv[idx + 1]29
if item.startswith(prefix):30
return item[len(prefix) :]31
return None34
def bootstrap_storage_env_from_argv(argv: Sequence[str]) -> None:35
raw_e_root = early_arg_value(argv, "--e_root") or E_ROOT36
try:37
e_root = resolve_e_root(raw_e_root)38
storage = ensure_storage_tree(e_root)39
apply_env_defaults(storage, set_temp_tmp=True)40
except Exception:41
return44
bootstrap_storage_env_from_argv(sys.argv[1:])46
import numpy as np47
import torch48
from torch.utils.data import Dataset49
from transformers import (50
AutoFeatureExtractor,51
Trainer,52
TrainingArguments,53
WhisperConfig,54
WhisperForConditionalGeneration,55
set_seed,56
)58
from ai_guitar_pedal.memory_guard import MemoryWarningCallback59
from ai_guitar_pedal.tokenizer.dsp_control_tokenizer import DSPControlTokenizer60
from ai_guitar_pedal.phase2_training_utils import (61
AUDIO_CHANNEL_MODE,62
PauseSaveAndStopCallback,63
STEREO_FEATURE_BINS,64
TARGET_SR,65
adapt_whisper_model_for_stereo,66
feature_cache_base_dir,67
feature_cache_path_for_audio,68
load_or_compute_input_features,69
preextract_feature_cache,70
resolve_pause_paths,71
resolve_resume_checkpoint,72
sanitize_experiment_name,73
str2bool,74
)77
def parse_args() -> argparse.Namespace:78
parser = argparse.ArgumentParser(79
description=(80
"Train Whisper audio-to-DSP-control-token sequence model. "81
"Targets are program model_token_ids, not preset numbers."82
)83
)84
parser.add_argument("--e_root", default=E_ROOT)85
parser.add_argument("--allow_non_e_root", type=str2bool, default=False)86
parser.add_argument("--dry_run", type=str2bool, default=False)87
parser.add_argument(88
"--validate_only",89
type=str2bool,90
default=False,91
help=(92
"Load and validate manifest/program-token rows, write split manifests, "93
"then exit before feature extraction/model training."94
),95
)96
parser.add_argument("--experiment_name", default="Phase2_ParamSeq_WhisperBase_V4_0001")97
parser.add_argument("--model_name", default="openai/whisper-base")98
parser.add_argument("--manifest", required=True)99
parser.add_argument(100
"--vocab",101
default=str(102
(103
Path(__file__).resolve().parent104
/ "resources"105
/ "dsp_control_vocab.json"106
).resolve()107
),108
)109
parser.add_argument(110
"--program_root",111
default=str(112
(113
Path(__file__).resolve().parent114
/ ".."115
/ "Audio"116
/ "Presets"117
/ "programs"118
).resolve()119
),120
help=(121
"Local fallback directory for program JSON files. Used when "122
"manifest target_program_json points at an unavailable absolute path."123
),124
)125
parser.add_argument("--audio_root", default="")126
parser.add_argument("--feature_cache_root", default="")127
parser.add_argument("--use_feature_cache", type=str2bool, default=True)128
parser.add_argument("--preextract_features", type=str2bool, default=True)129
parser.add_argument("--preextract_report_every", type=int, default=1000)130
parser.add_argument("--output_dir", default="")131
parser.add_argument("--log_dir", default="")132
parser.add_argument("--export_dir", default="")133
parser.add_argument("--max_seconds", type=float, default=6.0)134
parser.add_argument("--max_target_length", type=int, default=768)135
parser.add_argument("--strip_bos_from_labels", type=str2bool, default=True)136
parser.add_argument("--eval_ratio", type=float, default=0.02)137
parser.add_argument("--seed", type=int, default=42)138
parser.add_argument("--per_device_train_batch_size", type=int, default=1)139
parser.add_argument("--per_device_eval_batch_size", type=int, default=1)140
parser.add_argument("--gradient_accumulation_steps", type=int, default=32)141
parser.add_argument("--learning_rate", type=float, default=3e-5)142
parser.add_argument("--num_train_epochs", type=float, default=3)143
parser.add_argument("--max_steps", type=int, default=-1)144
parser.add_argument("--fp16", type=str2bool, default=True)145
parser.add_argument("--gradient_checkpointing", type=str2bool, default=True)146
parser.add_argument("--gpu_mem_frac", type=float, default=0.90)147
parser.add_argument("--num_workers", type=int, default=0)148
parser.add_argument("--prefetch_factor", type=int, default=2)149
parser.add_argument(150
"--checkpoint_strategy",151
choices=["epoch", "steps"],152
default="steps",153
)154
parser.add_argument("--save_steps", type=int, default=250)155
parser.add_argument("--eval_steps", type=int, default=250)156
parser.add_argument("--save_total_limit", type=int, default=5)157
parser.add_argument("--eval_accumulation_steps", type=int, default=1)158
parser.add_argument("--auto_resume", type=str2bool, default=True)159
parser.add_argument("--resume_from_checkpoint", default="")160
parser.add_argument("--pause_flag_file", default="")161
parser.add_argument("--pause_ckpt_dir", default="")162
parser.add_argument("--mem_warn_enabled", type=str2bool, default=True)163
parser.add_argument("--mem_warn_ram_threshold", type=float, default=0.90)164
parser.add_argument("--mem_warn_vram_threshold", type=float, default=0.90)165
parser.add_argument("--mem_warn_check_steps", type=int, default=25)166
parser.add_argument("--mem_warn_repeat_steps", type=int, default=200)167
return parser.parse_args()170
def resolve_paths(args: argparse.Namespace) -> Tuple[Path, Dict[str, Path], Dict[str, str], Path, Path, Path, Path, Path, Path, Path, Path]:171
e_root = resolve_e_root(args.e_root)172
storage = ensure_storage_tree(e_root)173
env_effective = apply_env_defaults(storage, set_temp_tmp=True)175
exp = sanitize_experiment_name(args.experiment_name)176
manifest = Path(args.manifest).expanduser().resolve()177
vocab = Path(args.vocab).expanduser().resolve()178
program_root = Path(args.program_root).expanduser().resolve()179
audio_root = (180
Path(args.audio_root).expanduser().resolve()181
if str(args.audio_root).strip()182
else manifest.parent.resolve()183
)184
output_dir = (185
Path(args.output_dir).expanduser().resolve()186
if str(args.output_dir).strip()187
else (storage["artifacts_checkpoints"] / exp).resolve()188
)189
log_dir = (190
Path(args.log_dir).expanduser().resolve()191
if str(args.log_dir).strip()192
else (storage["artifacts_runs"] / exp).resolve()193
)194
export_dir = (195
Path(args.export_dir).expanduser().resolve()196
if str(args.export_dir).strip()197
else (storage["artifacts_exports"] / exp).resolve()198
)199
feature_cache_root = (200
Path(args.feature_cache_root).expanduser().resolve()201
if str(args.feature_cache_root).strip()202
else storage["cache_whisper_features"].resolve()203
)205
for path in [output_dir, log_dir, export_dir, feature_cache_root]:206
path.mkdir(parents=True, exist_ok=True)208
for name, path in [209
("output_dir", output_dir),210
("log_dir", log_dir),211
("export_dir", export_dir),212
("feature_cache_root", feature_cache_root),213
]:214
assert_under_root(path, e_root, name, allow_non_root=bool(args.allow_non_e_root))216
return (217
e_root,218
storage,219
env_effective,220
manifest,221
vocab,222
program_root,223
audio_root,224
output_dir,225
log_dir,226
export_dir,227
feature_cache_root,228
)231
def resolve_manifest_audio_path(manifest_dir: Path, raw_path: str) -> Path:232
path = Path(str(raw_path).strip())233
if path.is_absolute():234
return path.expanduser().resolve()235
return (manifest_dir / path).expanduser().resolve()238
def resolve_program_path(manifest_dir: Path, program_root: Path, raw_path: str) -> Path:239
path = Path(str(raw_path).strip())240
if path.is_absolute():241
resolved = path.expanduser().resolve()242
else:243
resolved = (manifest_dir / path).expanduser().resolve()244
if resolved.exists():245
return resolved246
fallback = (program_root / path.name).expanduser().resolve()247
return fallback250
def token_hash(token_ids: Sequence[int]) -> str:251
text = ",".join(str(int(x)) for x in token_ids)252
return hashlib.sha256(text.encode("utf-8")).hexdigest()255
def read_program_token_ids(256
program_path: Path,257
tokenizer: DSPControlTokenizer,258
strip_bos: bool,259
) -> List[int]:260
payload = json.loads(program_path.read_text(encoding="utf-8"))261
token_ids = payload.get("model_token_ids")262
if not isinstance(token_ids, list) or not token_ids:263
raise ValueError(f"program missing model_token_ids: {program_path}")264
out = [int(x) for x in token_ids]265
if strip_bos and out and out[0] == tokenizer.bos_token_id:266
out = out[1:]267
if not out or out[-1] != tokenizer.eos_token_id:268
raise ValueError(f"program token sequence must end with EOS: {program_path}")269
bad = [x for x in out if x < 0 or x >= tokenizer.vocab_size]270
if bad:271
raise ValueError(f"program token id out of vocabulary: {program_path}, examples={bad[:10]}")272
return out275
def load_program_rows(276
manifest_path: Path,277
program_root: Path,278
tokenizer: DSPControlTokenizer,279
strip_bos: bool,280
max_target_length: int,281
) -> List[Dict[str, Any]]:282
manifest_path = manifest_path.expanduser().resolve()283
manifest_dir = manifest_path.parent284
program_cache: Dict[Path, Dict[str, Any]] = {}285
rows: List[Dict[str, Any]] = []286
invalid: List[str] = []287
missing: List[str] = []289
with manifest_path.open("r", encoding="utf-8") as handle:290
for line_no, line in enumerate(handle, start=1):291
text = line.strip()292
if not text:293
continue294
try:295
item = json.loads(text)296
except json.JSONDecodeError as exc:297
invalid.append(f"line {line_no}: invalid JSON ({exc})")298
continue299
if not isinstance(item, dict):300
invalid.append(f"line {line_no}: expected object")301
continue303
raw_audio = item.get("audio_filepath")304
raw_program = item.get("target_program_json")305
if not isinstance(raw_audio, str) or not raw_audio.strip():306
invalid.append(f"line {line_no}: missing audio_filepath")307
continue308
if not isinstance(raw_program, str) or not raw_program.strip():309
invalid.append(f"line {line_no}: missing target_program_json")310
continue312
audio_path = resolve_manifest_audio_path(manifest_dir, raw_audio)313
program_path = resolve_program_path(manifest_dir, program_root, raw_program)314
if not audio_path.exists():315
missing.append(f"line {line_no}: audio not found: {audio_path}")316
continue317
if not program_path.exists():318
missing.append(f"line {line_no}: program not found: {program_path}")319
continue321
if program_path not in program_cache:322
token_ids = read_program_token_ids(323
program_path=program_path,324
tokenizer=tokenizer,325
strip_bos=strip_bos,326
)327
if len(token_ids) > max_target_length:328
invalid.append(329
f"line {line_no}: target length {len(token_ids)} > max_target_length "330
f"{max_target_length}: {program_path}"331
)332
continue333
program_cache[program_path] = {334
"labels": token_ids,335
"program_hash": token_hash(token_ids),336
"target_length": len(token_ids),337
}339
cached = program_cache[program_path]340
rows.append(341
{342
"audio": str(audio_path),343
"labels": cached["labels"],344
"program_hash": cached["program_hash"],345
"target_length": cached["target_length"],346
}347
)349
if invalid or missing:350
lines = [f"Invalid program training manifest: {manifest_path}"]351
if invalid:352
lines.append(f"- Field/token errors: {len(invalid)}")353
lines.extend(f" {x}" for x in invalid[:100])354
if len(invalid) > 100:355
lines.append(f" ... and {len(invalid) - 100} more")356
if missing:357
lines.append(f"- Missing files: {len(missing)}")358
lines.extend(f" {x}" for x in missing[:100])359
if len(missing) > 100:360
lines.append(f" ... and {len(missing) - 100} more")361
raise ValueError("\n".join(lines))362
if not rows:363
raise ValueError(f"No usable rows found in manifest: {manifest_path}")364
return rows367
def split_by_program_hash(368
rows: Sequence[Dict[str, Any]],369
eval_ratio: float,370
seed: int,371
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:372
if not (0.0 <= eval_ratio < 1.0):373
raise ValueError(f"eval_ratio must be in [0,1), got {eval_ratio}")374
grouped: Dict[str, List[Dict[str, Any]]] = defaultdict(list)375
for row in rows:376
grouped[str(row["program_hash"])].append(row)378
keys = sorted(grouped)379
rng = random.Random(seed)380
rng.shuffle(keys)381
if eval_ratio <= 0.0 or len(keys) <= 1:382
eval_keys = set()383
else:384
eval_count = int(round(len(keys) * eval_ratio))385
eval_count = max(1, eval_count)386
eval_count = min(eval_count, len(keys) - 1)387
eval_keys = set(keys[:eval_count])389
train_rows: List[Dict[str, Any]] = []390
eval_rows: List[Dict[str, Any]] = []391
for key in keys:392
if key in eval_keys:393
eval_rows.extend(grouped[key])394
else:395
train_rows.extend(grouped[key])396
rng.shuffle(train_rows)397
rng.shuffle(eval_rows)398
if not train_rows:399
raise ValueError("No train rows after program-hash split.")400
return train_rows, eval_rows403
def write_split_manifest(path: Path, rows: Sequence[Dict[str, Any]]) -> None:404
path.parent.mkdir(parents=True, exist_ok=True)405
with path.open("w", encoding="utf-8") as handle:406
for row in rows:407
handle.write(408
json.dumps(409
{410
"audio": row["audio"],411
"program_hash": row["program_hash"],412
"target_length": int(row["target_length"]),413
},414
ensure_ascii=False,415
)416
+ "\n"417
)420
class ProgramSeq2SeqDataset(Dataset):421
def __init__(422
self,423
rows: Sequence[Dict[str, Any]],424
feature_extractor: AutoFeatureExtractor,425
max_seconds: float,426
audio_root: Path,427
use_feature_cache: bool,428
cache_base: Path,429
) -> None:430
self.rows = list(rows)431
self.feature_extractor = feature_extractor432
self.max_len = int(max_seconds * TARGET_SR)433
self.audio_root = audio_root434
self.use_feature_cache = bool(use_feature_cache)435
self.cache_base = cache_base436
if self.max_len <= 0:437
raise ValueError(f"max_seconds must produce positive max_len, got {max_seconds}")439
def __len__(self) -> int:440
return len(self.rows)442
def __getitem__(self, idx: int) -> Dict[str, torch.Tensor]:443
row = self.rows[idx]444
wav_path = Path(str(row["audio"])).expanduser().resolve()445
cache_path = feature_cache_path_for_audio(446
wav_path=wav_path,447
audio_root=self.audio_root,448
cache_base=self.cache_base,449
)450
input_features = load_or_compute_input_features(451
wav_path=wav_path,452
feature_extractor=self.feature_extractor,453
max_len=self.max_len,454
use_feature_cache=self.use_feature_cache,455
cache_path=cache_path,456
save_on_miss=self.use_feature_cache,457
)458
return {459
"input_features": input_features,460
"labels": torch.tensor(row["labels"], dtype=torch.long),461
}464
class Seq2SeqCollator:465
def __init__(self, pad_token_id: int, label_pad_token_id: int = -100) -> None:466
self.pad_token_id = int(pad_token_id)467
self.label_pad_token_id = int(label_pad_token_id)469
def __call__(self, items: List[Dict[str, torch.Tensor]]) -> Dict[str, torch.Tensor]:470
input_features = torch.stack([item["input_features"] for item in items], dim=0)471
labels = [item["labels"] for item in items]472
max_len = max(int(label.shape[0]) for label in labels)473
padded = torch.full(474
(len(labels), max_len),475
self.label_pad_token_id,476
dtype=torch.long,477
)478
for idx, label in enumerate(labels):479
padded[idx, : label.shape[0]] = label480
return {"input_features": input_features, "labels": padded}483
def preprocess_logits_for_metrics(logits, labels):484
if isinstance(logits, tuple):485
logits = logits[0]486
return torch.argmax(logits, dim=-1)489
def compute_token_metrics(eval_pred) -> Dict[str, float]:490
preds = np.asarray(eval_pred.predictions)491
labels = np.asarray(eval_pred.label_ids)492
if preds.shape != labels.shape:493
preds = preds.reshape(labels.shape)494
mask = labels != -100495
if not np.any(mask):496
return {"token_acc": 0.0, "seq_exact": 0.0}497
token_acc = float(np.mean(preds[mask] == labels[mask]))498
seq_ok = []499
for pred_row, label_row, mask_row in zip(preds, labels, mask):500
seq_ok.append(bool(np.all(pred_row[mask_row] == label_row[mask_row])))501
return {502
"token_acc": token_acc,503
"seq_exact": float(np.mean(seq_ok)) if seq_ok else 0.0,504
}507
def build_model(args: argparse.Namespace, tokenizer: DSPControlTokenizer) -> WhisperForConditionalGeneration:508
config = WhisperConfig.from_pretrained(args.model_name)509
requested_target_positions = int(args.max_target_length)510
if requested_target_positions <= 0:511
raise ValueError(f"max_target_length must be positive, got {requested_target_positions}")512
config.max_target_positions = max(513
requested_target_positions,514
int(getattr(config, "max_target_positions", 0) or 0),515
)516
config.vocab_size = int(tokenizer.vocab_size)517
config.pad_token_id = int(tokenizer.pad_token_id)518
config.bos_token_id = int(tokenizer.bos_token_id)519
config.eos_token_id = int(tokenizer.eos_token_id)520
config.decoder_start_token_id = int(tokenizer.bos_token_id)521
config.forced_decoder_ids = None522
config.suppress_tokens = []523
config.begin_suppress_tokens = []525
model = WhisperForConditionalGeneration.from_pretrained(526
args.model_name,527
config=config,528
ignore_mismatched_sizes=True,529
)530
adapt_whisper_model_for_stereo(model)531
model.config.use_cache = False532
if hasattr(model, "generation_config"):533
model.generation_config.pad_token_id = int(tokenizer.pad_token_id)534
model.generation_config.bos_token_id = int(tokenizer.bos_token_id)535
model.generation_config.eos_token_id = int(tokenizer.eos_token_id)536
model.generation_config.decoder_start_token_id = int(tokenizer.bos_token_id)537
model.generation_config.forced_decoder_ids = None538
model.generation_config.suppress_tokens = []539
model.generation_config.begin_suppress_tokens = []540
model.generation_config.max_length = int(args.max_target_length)541
if args.gradient_checkpointing:542
model.gradient_checkpointing_enable()543
return model546
def build_training_args(args: argparse.Namespace, has_eval: bool) -> TrainingArguments:547
strategy = str(args.checkpoint_strategy)548
if strategy == "steps":549
if int(args.save_steps) <= 0:550
raise ValueError("save_steps must be > 0")551
eval_steps = int(args.eval_steps) if int(args.eval_steps) > 0 else int(args.save_steps)552
if int(args.save_steps) % int(eval_steps) != 0:553
raise ValueError("save_steps must be a round multiple of eval_steps")554
else:555
eval_steps = 0557
kwargs: Dict[str, Any] = {558
"output_dir": str(Path(args.output_dir).expanduser().resolve()),559
"logging_dir": str(Path(args.log_dir).expanduser().resolve()),560
"per_device_train_batch_size": int(args.per_device_train_batch_size),561
"per_device_eval_batch_size": int(args.per_device_eval_batch_size),562
"gradient_accumulation_steps": int(args.gradient_accumulation_steps),563
"learning_rate": float(args.learning_rate),564
"num_train_epochs": float(args.num_train_epochs),565
"max_steps": int(args.max_steps),566
"fp16": bool(args.fp16 and torch.cuda.is_available()),567
"save_strategy": strategy,568
"logging_strategy": "steps",569
"logging_steps": 10,570
"save_total_limit": max(1, int(args.save_total_limit)),571
"report_to": [],572
"run_name": sanitize_experiment_name(args.experiment_name),573
"remove_unused_columns": False,574
"seed": int(args.seed),575
"gradient_checkpointing": bool(args.gradient_checkpointing),576
"dataloader_num_workers": int(args.num_workers),577
"dataloader_pin_memory": True,578
"eval_accumulation_steps": max(1, int(args.eval_accumulation_steps)),579
}580
if int(args.max_steps) <= 0:581
kwargs.pop("max_steps")583
params = inspect.signature(TrainingArguments.__init__).parameters584
if "eval_strategy" in params:585
kwargs["eval_strategy"] = strategy if has_eval else "no"586
else:587
kwargs["evaluation_strategy"] = strategy if has_eval else "no"588
if strategy == "steps":589
kwargs["save_steps"] = int(args.save_steps)590
if has_eval:591
kwargs["eval_steps"] = int(eval_steps)592
if has_eval:593
kwargs["load_best_model_at_end"] = True594
kwargs["metric_for_best_model"] = "eval_token_acc"595
kwargs["greater_is_better"] = True596
if "dataloader_persistent_workers" in params:597
kwargs["dataloader_persistent_workers"] = bool(args.num_workers > 0)598
if "dataloader_prefetch_factor" in params and args.num_workers > 0:599
kwargs["dataloader_prefetch_factor"] = max(1, int(args.prefetch_factor))600
return TrainingArguments(**kwargs)603
def main() -> None:604
args = parse_args()605
(606
e_root,607
storage,608
env_effective,609
manifest_path,610
vocab_path,611
program_root,612
audio_root,613
output_dir,614
log_dir,615
export_dir,616
feature_cache_root,617
) = resolve_paths(args)619
args.manifest = str(manifest_path)620
args.vocab = str(vocab_path)621
args.program_root = str(program_root)622
args.audio_root = str(audio_root)623
args.output_dir = str(output_dir)624
args.log_dir = str(log_dir)625
args.export_dir = str(export_dir)626
args.feature_cache_root = str(feature_cache_root)628
print("[ParamSeq] Effective paths")629
print(f" e_root={e_root}")630
print(f" experiment_name={sanitize_experiment_name(args.experiment_name)}")631
print(f" manifest={manifest_path}")632
print(f" vocab={vocab_path}")633
print(f" program_root={program_root}")634
print(f" audio_root={audio_root}")635
print(f" output_dir={output_dir}")636
print(f" log_dir={log_dir}")637
print(f" export_dir={export_dir}")638
print(f" feature_cache_root={feature_cache_root}")640
if args.dry_run:641
print("[DryRun] Exiting before dataset scan and training.")642
return644
if not manifest_path.exists():645
raise FileNotFoundError(f"manifest not found: {manifest_path}")646
if not vocab_path.exists():647
raise FileNotFoundError(f"vocab not found: {vocab_path}")648
if not program_root.exists():649
raise FileNotFoundError(f"program_root not found: {program_root}")650
if not audio_root.exists():651
raise FileNotFoundError(f"audio_root not found: {audio_root}")653
set_seed(int(args.seed))654
tokenizer = DSPControlTokenizer.from_vocab_file(vocab_path)656
pause_flag_file, pause_ckpt_dir = resolve_pause_paths(args, output_dir)657
assert_under_root(658
pause_ckpt_dir,659
e_root,660
"pause_ckpt_dir",661
allow_non_root=bool(args.allow_non_e_root),662
)664
if args.gpu_mem_frac and torch.cuda.is_available():665
if not (0.0 < float(args.gpu_mem_frac) <= 1.0):666
raise ValueError(f"gpu_mem_frac must be in (0,1], got {args.gpu_mem_frac}")667
if hasattr(torch.cuda, "memory") and hasattr(668
torch.cuda.memory, "set_per_process_memory_fraction"669
):670
torch.cuda.memory.set_per_process_memory_fraction(float(args.gpu_mem_frac), device=0)671
else:672
torch.cuda.set_per_process_memory_fraction(float(args.gpu_mem_frac), device=0)673
total_mem_gb = torch.cuda.get_device_properties(0).total_memory / 1024**3674
print(675
f"[GPU] total={total_mem_gb:.2f}GB, "676
f"limit={float(args.gpu_mem_frac):.2f}, "677
f"allowed~{total_mem_gb * float(args.gpu_mem_frac):.2f}GB"678
)680
t0 = time.time()681
rows = load_program_rows(682
manifest_path=manifest_path,683
program_root=program_root,684
tokenizer=tokenizer,685
strip_bos=bool(args.strip_bos_from_labels),686
max_target_length=int(args.max_target_length),687
)688
elapsed = time.time() - t0689
lengths = [int(row["target_length"]) for row in rows]690
program_hash_count = len({str(row["program_hash"]) for row in rows})691
print(692
f"[ParamSeq] loaded rows={len(rows)}, unique_program_hashes={program_hash_count}, "693
f"target_len_min={min(lengths)}, target_len_max={max(lengths)}, "694
f"target_len_mean={float(np.mean(lengths)):.2f}, elapsed={elapsed:.1f}s"695
)697
train_rows, eval_rows = split_by_program_hash(rows, float(args.eval_ratio), int(args.seed))698
print(699
f"[ParamSeq] split by program_hash: train_rows={len(train_rows)}, "700
f"eval_rows={len(eval_rows)}, train_programs={len({r['program_hash'] for r in train_rows})}, "701
f"eval_programs={len({r['program_hash'] for r in eval_rows})}"702
)703
write_split_manifest(output_dir / "train_split_desensitized.jsonl", train_rows)704
write_split_manifest(output_dir / "eval_split_desensitized.jsonl", eval_rows)705
if args.validate_only:706
print("[ValidateOnly] Manifest/program-token validation passed.")707
print(f"[ValidateOnly] Split manifests written under: {output_dir}")708
return710
feature_extractor = AutoFeatureExtractor.from_pretrained(args.model_name)711
print(f"Audio channel mode: {AUDIO_CHANNEL_MODE}")712
print(f"Audio feature bins: {STEREO_FEATURE_BINS}")713
cache_base = feature_cache_base_dir(714
feature_cache_root=feature_cache_root,715
model_name=args.model_name,716
max_seconds=float(args.max_seconds),717
)718
if args.use_feature_cache and args.preextract_features:719
preextract_feature_cache(720
rows=rows,721
audio_root=audio_root,722
feature_extractor=feature_extractor,723
max_len=int(float(args.max_seconds) * TARGET_SR),724
cache_base=cache_base,725
report_every=int(args.preextract_report_every),726
)728
train_dataset = ProgramSeq2SeqDataset(729
rows=train_rows,730
feature_extractor=feature_extractor,731
max_seconds=float(args.max_seconds),732
audio_root=audio_root,733
use_feature_cache=bool(args.use_feature_cache),734
cache_base=cache_base,735
)736
eval_dataset = (737
ProgramSeq2SeqDataset(738
rows=eval_rows,739
feature_extractor=feature_extractor,740
max_seconds=float(args.max_seconds),741
audio_root=audio_root,742
use_feature_cache=bool(args.use_feature_cache),743
cache_base=cache_base,744
)745
if eval_rows746
else None747
)749
model = build_model(args, tokenizer)750
trainer = Trainer(751
model=model,752
args=build_training_args(args, has_eval=eval_dataset is not None),753
train_dataset=train_dataset,754
eval_dataset=eval_dataset,755
data_collator=Seq2SeqCollator(pad_token_id=tokenizer.pad_token_id),756
compute_metrics=compute_token_metrics if eval_dataset is not None else None,757
preprocess_logits_for_metrics=preprocess_logits_for_metrics758
if eval_dataset is not None759
else None,760
)761
if args.mem_warn_enabled:762
trainer.add_callback(763
MemoryWarningCallback(764
ram_threshold=float(args.mem_warn_ram_threshold),765
vram_threshold=float(args.mem_warn_vram_threshold),766
check_every_steps=int(args.mem_warn_check_steps),767
repeat_warning_steps=int(args.mem_warn_repeat_steps),768
)769
)771
pause_callback = PauseSaveAndStopCallback(772
pause_flag_file=pause_flag_file,773
pause_ckpt_dir=pause_ckpt_dir,774
remove_pause_flag=True,775
)776
pause_callback.set_trainer(trainer)777
trainer.add_callback(pause_callback)779
resume_ckpt, resume_source = resolve_resume_checkpoint(args, pause_ckpt_dir)780
if resume_ckpt is not None:781
print(f"[Resume] source={resume_source}, resume_from_checkpoint={resume_ckpt}")782
trainer.train(resume_from_checkpoint=str(resume_ckpt))783
else:784
print(f"[Resume] source={resume_source}, starting from scratch")785
trainer.train()787
if eval_dataset is not None:788
metrics = trainer.evaluate()789
print(790
f"[Eval] eval_token_acc={metrics.get('eval_token_acc', 0.0):.6f}, "791
f"eval_seq_exact={metrics.get('eval_seq_exact', 0.0):.6f}"792
)793
else:794
metrics = {}796
export_dir.mkdir(parents=True, exist_ok=True)797
trainer.save_model(str(export_dir))798
feature_extractor.save_pretrained(str(export_dir))799
shutil.copy2(vocab_path, export_dir / "dsp_control_vocab.json")801
summary = {802
"objective": "audio_to_dsp_control_program_tokens",803
"desensitized_from_preset_number": True,804
"uses_preset_number_as_label": False,805
"target_source_field": "target_program_json.model_token_ids",806
"manifest": str(manifest_path),807
"vocab": str(vocab_path),808
"program_root": str(program_root),809
"vocab_size": int(tokenizer.vocab_size),810
"audio_root": str(audio_root),811
"total_rows": int(len(rows)),812
"train_rows": int(len(train_rows)),813
"eval_rows": int(len(eval_rows)),814
"unique_program_hashes": int(program_hash_count),815
"train_program_hashes": int(len({r["program_hash"] for r in train_rows})),816
"eval_program_hashes": int(len({r["program_hash"] for r in eval_rows})),817
"target_length": {818
"min": int(min(lengths)),819
"max": int(max(lengths)),820
"mean": float(np.mean(lengths)),821
},822
"metrics": metrics,823
"training_config": {824
"model_name": str(args.model_name),825
"audio_channel_mode": AUDIO_CHANNEL_MODE,826
"input_feature_bins": int(STEREO_FEATURE_BINS),827
"max_seconds": float(args.max_seconds),828
"max_target_length": int(args.max_target_length),829
"strip_bos_from_labels": bool(args.strip_bos_from_labels),830
"eval_ratio": float(args.eval_ratio),831
"num_train_epochs": float(args.num_train_epochs),832
"max_steps": int(args.max_steps),833
"checkpoint_strategy": str(args.checkpoint_strategy),834
"save_steps": int(args.save_steps),835
"eval_steps": int(args.eval_steps),836
"save_total_limit": int(args.save_total_limit),837
"per_device_train_batch_size": int(args.per_device_train_batch_size),838
"per_device_eval_batch_size": int(args.per_device_eval_batch_size),839
"gradient_accumulation_steps": int(args.gradient_accumulation_steps),840
"learning_rate": float(args.learning_rate),841
"gradient_checkpointing": bool(args.gradient_checkpointing),842
"fp16": bool(args.fp16 and torch.cuda.is_available()),843
"gpu_mem_frac": float(args.gpu_mem_frac),844
"auto_resume": bool(args.auto_resume),845
"resume_from_checkpoint": str(resume_ckpt) if resume_ckpt is not None else "",846
"resume_source": str(resume_source),847
},848
"feature_cache_base": str(cache_base),849
"e_root": str(e_root),850
"output_dir": str(output_dir),851
"log_dir": str(log_dir),852
"export_dir": str(export_dir),853
"pause_flag_file": str(pause_flag_file),854
"pause_ckpt_dir": str(pause_ckpt_dir),855
"env": env_effective,856
}857
(export_dir / "run_summary.json").write_text(858
json.dumps(summary, indent=2, ensure_ascii=False),859
encoding="utf-8",860
)861
print(f"Parameter sequence model exported to: {export_dir}")864
if __name__ == "__main__":865
main()