Add 'Train with your agent' hand-off button
Browse files
app.py
CHANGED
|
@@ -100,6 +100,23 @@ button.primary {box-shadow:0 6px 22px rgba(255,140,50,0.28);}
|
|
| 100 |
/* file drop zone: a single input-shade panel (kill the lighter inner button) + readable text */
|
| 101 |
#ds-upload {background:rgba(255,255,255,0.04) !important;}
|
| 102 |
#ds-upload * {background-color:transparent !important; color:#cfcabf !important;}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
footer {visibility:hidden;}
|
| 104 |
"""
|
| 105 |
|
|
@@ -169,6 +186,111 @@ PROFILES = {
|
|
| 169 |
}
|
| 170 |
|
| 171 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
def _apply_profile(profile: str, rank_dirty: bool, alpha_dirty: bool, flavor_dirty: bool):
|
| 173 |
"""Cascade a profile to (quantization, optimizer, te_8bit, rank, alpha, manual-group visibility,
|
| 174 |
flavor). rank/alpha/flavor respect dirty flags (preserve manual edits); Custom reveals manual
|
|
@@ -269,10 +391,31 @@ with gr.Blocks(title="LTX-2.3 LoRA Trainer") as demo:
|
|
| 269 |
"Train a LoRA / IC-LoRA on your own videos β runs on **HF Jobs**, pushed to your Hub.",
|
| 270 |
elem_id="hero",
|
| 271 |
)
|
| 272 |
-
gr.
|
|
|
|
|
|
|
|
|
|
| 273 |
|
| 274 |
banner = gr.Markdown(elem_id="banner")
|
| 275 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
with gr.Tabs():
|
| 277 |
# ----------------------------------------------------------------- 1 Β· Dataset
|
| 278 |
with gr.Tab("1 Β· Dataset"):
|
|
@@ -410,6 +553,10 @@ with gr.Blocks(title="LTX-2.3 LoRA Trainer") as demo:
|
|
| 410 |
).then(_extract_id, inputs=joblink, outputs=job_id)
|
| 411 |
refresh_btn.click(refresh, inputs=[job_id], outputs=[mon_status, mon_logs])
|
| 412 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
|
| 414 |
if __name__ == "__main__":
|
| 415 |
demo.queue(default_concurrency_limit=2).launch(theme=THEME, css=CSS)
|
|
|
|
| 100 |
/* file drop zone: a single input-shade panel (kill the lighter inner button) + readable text */
|
| 101 |
#ds-upload {background:rgba(255,255,255,0.04) !important;}
|
| 102 |
#ds-upload * {background-color:transparent !important; color:#cfcabf !important;}
|
| 103 |
+
/* agent hand-off modal β CSS overlay on a gr.Column, shown/hidden via its `visible` toggle.
|
| 104 |
+
`visible=False` sets display:none (overrides this id's display:flex), so it stays hidden. */
|
| 105 |
+
#agent-modal {position:fixed; inset:0; z-index:1000; display:flex; align-items:center; justify-content:center;
|
| 106 |
+
background:rgba(0,0,0,0.55) !important; backdrop-filter:blur(4px); border:none !important; padding:18px;}
|
| 107 |
+
/* our id selector (specificity 100) would otherwise beat gradio's `.hide` (10) and keep the
|
| 108 |
+
modal on screen after Close β this restores the hide when `visible=False` adds `.hide`. */
|
| 109 |
+
#agent-modal.hide {display:none !important;}
|
| 110 |
+
#agent-modal-card {max-width:940px; width:94%; max-height:88vh; overflow:auto;
|
| 111 |
+
background:#1e2024 !important; border:1px solid rgba(255,255,255,0.18) !important;
|
| 112 |
+
border-radius:16px !important; padding:22px 24px !important;
|
| 113 |
+
box-shadow:0 24px 80px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.05);}
|
| 114 |
+
#agent-open-btn {white-space:nowrap;}
|
| 115 |
+
/* tighten the modal headingβtext gap and make inline `code` readable on the dark card */
|
| 116 |
+
#agent-modal-card h1, #agent-modal-card h2, #agent-modal-card h3 {margin:0 0 4px !important;}
|
| 117 |
+
#agent-modal-card p {margin:.15rem 0 !important;}
|
| 118 |
+
#agent-modal-card code {background:rgba(255,255,255,0.12) !important; color:#f2efe9 !important;
|
| 119 |
+
padding:1px 6px; border-radius:5px;}
|
| 120 |
footer {visibility:hidden;}
|
| 121 |
"""
|
| 122 |
|
|
|
|
| 186 |
}
|
| 187 |
|
| 188 |
|
| 189 |
+
def _fmt_lr(v: float) -> str:
|
| 190 |
+
"""2e-4 / 1e-4 style (matches the LR field's helper text)."""
|
| 191 |
+
return f"{v:.0e}".replace("e-0", "e-").replace("e+0", "e+")
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def _build_agent_playbook() -> str:
|
| 195 |
+
"""Assemble the copy-paste agent playbook from the SAME constants the UI uses
|
| 196 |
+
(jobs.MODES, PROFILES, MODE_HELP, FLAVOR_GUIDE, RESOLUTION_PRESETS) so its tips and
|
| 197 |
+
recipe tables never drift from the live app. Fully generic: no user data, no token β
|
| 198 |
+
the agent reads HF_TOKEN from its own environment."""
|
| 199 |
+
mode_rows = "\n".join(
|
| 200 |
+
f"| {m} | {_fmt_lr(jobs.MODES[m]['recommended']['learning_rate'])} | "
|
| 201 |
+
f"{jobs.MODES[m]['recommended']['steps']} |"
|
| 202 |
+
for m in MODE_KEYS
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
def _prow(name: str, p: dict) -> str:
|
| 206 |
+
return (f"| {name} | {p['quantization']} | {p['optimizer_type']} | "
|
| 207 |
+
f"{'on' if p['te_8bit'] else 'off'} | {p['rank']} | {p['alpha']} | {p['flavor']} |")
|
| 208 |
+
|
| 209 |
+
profile_rows = "\n".join([
|
| 210 |
+
_prow(PROFILE_QUALITY, PROFILES[PROFILE_QUALITY]),
|
| 211 |
+
_prow(PROFILE_LOWVRAM, PROFILES[PROFILE_LOWVRAM]),
|
| 212 |
+
f"| {PROFILE_CUSTOM} | (you choose) | (you choose) | (you choose) | 32 | 32 | (you choose) |",
|
| 213 |
+
])
|
| 214 |
+
presets = " Β· ".join(f"`{v}` ({label.split('Β·')[-1].strip()})" for label, v in RESOLUTION_PRESETS)
|
| 215 |
+
|
| 216 |
+
return f"""# Train an LTX-2.3 LoRA for me (ltx-community/ltx2-lora-trainer)
|
| 217 |
+
|
| 218 |
+
You're acting as the trainer Space's UI, but conversational. Walk me through the 4
|
| 219 |
+
steps below **one at a time**. After I answer, apply the suggested defaults from the
|
| 220 |
+
tables (I can override any of them), then submit the job *exactly the way the Space
|
| 221 |
+
does* (Step 4). Confirm my final choices before launching.
|
| 222 |
+
|
| 223 |
+
## Step 1 β Dataset
|
| 224 |
+
{MODE_HELP}
|
| 225 |
+
|
| 226 |
+
- **Dataset source**: a Hub dataset repo (trainer-format `dataset.json`: `media_path`
|
| 227 |
+
+ `caption` [+ `reference_video` for IC-LoRA]), OR a local folder of clips I point you to.
|
| 228 |
+
- **Captioning**: drop a `clip.txt` next to each clip for a per-clip caption; otherwise a
|
| 229 |
+
shared caption is used. LTX-2.3 likes long, detailed, *chronological* captions (~200 words:
|
| 230 |
+
motion, camera, lighting, audio). A **trigger word** is prepended to every caption so I can
|
| 231 |
+
invoke the LoRA at inference. For IC-LoRA, every target `X.mp4` needs a paired `X_reference.mp4`.
|
| 232 |
+
|
| 233 |
+
**Apply on mode selection (editable):**
|
| 234 |
+
| mode | learning_rate | steps |
|
| 235 |
+
|---|---|---|
|
| 236 |
+
{mode_rows}
|
| 237 |
+
|
| 238 |
+
## Step 2 β Training (defaults auto-applied from mode + profile β tweak freely)
|
| 239 |
+
- **Resolution** `WxHxF` (default `768x512x49`). Rule: W,H divisible by 32; `F % 8 == 1`.
|
| 240 |
+
Presets: {presets}.
|
| 241 |
+
- **Rank/alpha**: keep alpha = rank, range 8β128. **LR/steps** come from the mode table above.
|
| 242 |
+
- **Advanced**: batch size 1 (required for multi-resolution datasets), grad accumulation 1,
|
| 243 |
+
validate every 250 steps (inference uses 30 steps).
|
| 244 |
+
|
| 245 |
+
**Apply on performance-profile selection (editable):**
|
| 246 |
+
| profile | quantization | optimizer | TE 8-bit | rank | alpha | GPU flavor |
|
| 247 |
+
|---|---|---|---|---|---|---|
|
| 248 |
+
{profile_rows}
|
| 249 |
+
|
| 250 |
+
## Step 3 β Launch
|
| 251 |
+
- **Run name**, whether to **push** the LoRA to my Hub, and the **hub model id**.
|
| 252 |
+
- **GPU flavor** (the profile suggests one; change freely):
|
| 253 |
+
|
| 254 |
+
{FLAVOR_GUIDE}
|
| 255 |
+
- **Timeout** (default `6h`). The first run spends ~minutes downloading the model.
|
| 256 |
+
|
| 257 |
+
## Step 4 β Submit & monitor (use the Space's own backend)
|
| 258 |
+
Use MY Hugging Face token from the environment (`hf auth login` or `HF_TOKEN`) β never
|
| 259 |
+
hard-code it. Fill `params` with my resolved answers from Steps 1β3, then run:
|
| 260 |
+
|
| 261 |
+
# pip install "huggingface_hub>=1.5" pyyaml hf_xet
|
| 262 |
+
import os, sys, time
|
| 263 |
+
from huggingface_hub import snapshot_download
|
| 264 |
+
sys.path.insert(0, snapshot_download("ltx-community/ltx2-lora-trainer", repo_type="space"))
|
| 265 |
+
import jobs
|
| 266 |
+
|
| 267 |
+
params = {{
|
| 268 |
+
"mode": "<mode from Step 1>",
|
| 269 |
+
"dataset_repo": "<hub dataset, or '' if passing local files below>",
|
| 270 |
+
"run_name": "ltx2-lora",
|
| 271 |
+
"resolution": "768x512x49",
|
| 272 |
+
"rank": 32, "alpha": 32, # from profile table
|
| 273 |
+
"learning_rate": 2e-4, "steps": 3000, # from mode table
|
| 274 |
+
"batch_size": 1, "gradient_accumulation_steps": 1, "validation_interval": 250,
|
| 275 |
+
"quantization": None, "optimizer_type": "adamw", "load_text_encoder_in_8bit": False,
|
| 276 |
+
"push_to_hub": True, "hub_model_id": "USERNAME/my-lora",
|
| 277 |
+
"caption_all": "", "trigger_word": "", "seed": 42,
|
| 278 |
+
"hf_token": os.environ["HF_TOKEN"],
|
| 279 |
+
}}
|
| 280 |
+
# Hub dataset -> [] ; local clips -> ["clip1.mp4", "clip1_reference.mp4", ...]
|
| 281 |
+
res = jobs.submit(params, [], flavor="rtx-pro-6000", timeout="6h")
|
| 282 |
+
job_id = res["job_id"]; print("Submitted:", res["url"] or job_id)
|
| 283 |
+
while True:
|
| 284 |
+
st = jobs.job_status(job_id, params["hf_token"]); print("status:", st)
|
| 285 |
+
if st.upper() in {{"COMPLETED", "ERROR", "CANCELLED", "FAILED"}}: break
|
| 286 |
+
time.sleep(30)
|
| 287 |
+
print(jobs.job_logs(job_id, params["hf_token"])[-4000:])
|
| 288 |
+
"""
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
AGENT_PLAYBOOK = _build_agent_playbook()
|
| 292 |
+
|
| 293 |
+
|
| 294 |
def _apply_profile(profile: str, rank_dirty: bool, alpha_dirty: bool, flavor_dirty: bool):
|
| 295 |
"""Cascade a profile to (quantization, optimizer, te_8bit, rank, alpha, manual-group visibility,
|
| 296 |
flavor). rank/alpha/flavor respect dirty flags (preserve manual edits); Custom reveals manual
|
|
|
|
| 391 |
"Train a LoRA / IC-LoRA on your own videos β runs on **HF Jobs**, pushed to your Hub.",
|
| 392 |
elem_id="hero",
|
| 393 |
)
|
| 394 |
+
with gr.Column(scale=0, min_width=210):
|
| 395 |
+
gr.LoginButton()
|
| 396 |
+
agent_open_btn = gr.Button("π€ Train with your agent", variant="secondary",
|
| 397 |
+
size="sm", elem_id="agent-open-btn")
|
| 398 |
|
| 399 |
banner = gr.Markdown(elem_id="banner")
|
| 400 |
|
| 401 |
+
# ββ agent hand-off modal ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 402 |
+
# CSS overlay (see #agent-modal) toggled by the hero button. Uses a Column, not a
|
| 403 |
+
# Group: a Group renders its elem_id on two nested divs and won't re-hide via
|
| 404 |
+
# `visible=False`, whereas a Column toggles cleanly. The snippet is a fully generic
|
| 405 |
+
# playbook built from the same constants the UI uses β no user data, no token
|
| 406 |
+
# (the agent reads HF_TOKEN from its own environment).
|
| 407 |
+
with gr.Column(visible=False, elem_id="agent-modal") as agent_modal:
|
| 408 |
+
with gr.Column(elem_id="agent-modal-card"):
|
| 409 |
+
gr.Markdown(
|
| 410 |
+
"### π€ Train with your agent\n"
|
| 411 |
+
"Copy the prompt below into your coding agent. It walks you through the same steps "
|
| 412 |
+
"as this UI, then launches training on **HF Jobs** under your account with your own "
|
| 413 |
+
"Hugging Face token (`hf auth login`) β no token or data is embedded here."
|
| 414 |
+
)
|
| 415 |
+
gr.Code(value=AGENT_PLAYBOOK, language="markdown", lines=20, max_lines=22,
|
| 416 |
+
wrap_lines=False, interactive=False, show_label=False, elem_id="agent-snippet")
|
| 417 |
+
agent_close_btn = gr.Button("Close", variant="secondary")
|
| 418 |
+
|
| 419 |
with gr.Tabs():
|
| 420 |
# ----------------------------------------------------------------- 1 Β· Dataset
|
| 421 |
with gr.Tab("1 Β· Dataset"):
|
|
|
|
| 553 |
).then(_extract_id, inputs=joblink, outputs=job_id)
|
| 554 |
refresh_btn.click(refresh, inputs=[job_id], outputs=[mon_status, mon_logs])
|
| 555 |
|
| 556 |
+
# agent hand-off modal open/close (no inputs β emits the same generic playbook every time)
|
| 557 |
+
agent_open_btn.click(lambda: gr.update(visible=True), outputs=agent_modal)
|
| 558 |
+
agent_close_btn.click(lambda: gr.update(visible=False), outputs=agent_modal)
|
| 559 |
+
|
| 560 |
|
| 561 |
if __name__ == "__main__":
|
| 562 |
demo.queue(default_concurrency_limit=2).launch(theme=THEME, css=CSS)
|