multimodalart HF Staff commited on
Commit
8bce267
Β·
verified Β·
1 Parent(s): 08c562a

agent: host /agent recipe + /train_job.py + /api/train; mount via FastAPI

Browse files
Files changed (1) hide show
  1. app.py +149 -1
app.py CHANGED
@@ -37,6 +37,122 @@ CONCEPT_CHOICES = [
37
  ("βš™οΈ Custom", "custom"),
38
  ]
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  CSS = """
41
  #main_title{text-align:center}
42
  #main_title h1{font-size:2.25rem;margin-bottom:0}
@@ -46,6 +162,8 @@ CSS = """
46
  .login_logout{width:100% !important}
47
  #login{width:100% !important;margin:0 auto}
48
  #signin_banner{text-align:center}
 
 
49
  #main_ui.locked{pointer-events:none;opacity:.55;filter:grayscale(.3)}
50
 
51
  /* captioning panel */
@@ -292,6 +410,9 @@ with gr.Blocks(title="Krea 2 LoRA Trainer") as demo:
292
  gr.Column(scale=2, min_width=0)
293
  with gr.Column(scale=0, min_width=380):
294
  gr.LoginButton(elem_id="login", elem_classes=["login_logout"])
 
 
 
295
  gr.Column(scale=2, min_width=0)
296
  banner = gr.Markdown(elem_id="signin_banner")
297
 
@@ -455,4 +576,31 @@ with gr.Blocks(title="Krea 2 LoRA Trainer") as demo:
455
 
456
 
457
  if __name__ == "__main__":
458
- demo.queue(default_concurrency_limit=4).launch(theme=THEME, css=CSS)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  ("βš™οΈ Custom", "custom"),
38
  ]
39
 
40
+ SPACE_HOST = os.environ.get("SPACE_HOST", "multimodalart-krea2-lora-trainer.hf.space")
41
+ AGENT_URL = f"https://{SPACE_HOST}/agent"
42
+
43
+ _AGENT_DOC = r'''# Train a Krea 2 LoRA β€” agent recipe
44
+
45
+ You can train a custom **Krea 2** LoRA on Hugging Face Jobs. Krea 2 weights are public, so you
46
+ only need the USER's own write-scoped HF token (https://huggingface.co/settings/tokens). Every
47
+ step below runs under that token and is billed to that account.
48
+
49
+ ## Inputs to collect from the user
50
+ - **lora_name** β€” a unique name. The result is pushed to `<user>/<lora_name>`.
51
+ - **trigger** β€” a style phrase (e.g. `"in the style of TOK"`) or, for a subject/face/object, a
52
+ rare token (e.g. `"TOK person"`). It is included in every training caption.
53
+ - **concept** β€” one of: `style` | `character` | `object` | `face`.
54
+ - **images** β€” 4–30 images. The user will give you ONE of:
55
+ (a) an existing HF dataset repo with `image` + `prompt` columns β†’ use it directly, skip Step 1;
56
+ (b) a local folder of images β†’ build the dataset (Step 1);
57
+ (c) a request like *"find impasto-style paintings"* β†’ gather the images yourself first, then
58
+ build the dataset (Step 1).
59
+
60
+ ## Step 1 β€” build the dataset (skip if the user already has one)
61
+ Create an imagefolder dataset under the user's account; each row is an image + a caption that
62
+ contains the trigger.
63
+
64
+ ```python
65
+ import json, os
66
+ from pathlib import Path
67
+ from huggingface_hub import HfApi
68
+
69
+ TOKEN = os.environ["HF_TOKEN"] # the user's write token
70
+ REPO = "<user>/<lora_name>-dataset"
71
+ TRIGGER = "<trigger>"
72
+ images = ["img1.png", "img2.png"] # local paths you gathered
73
+ captions = ["a cat on a sofa, " + TRIGGER, "a cat by a window, " + TRIGGER] # 1 per image
74
+
75
+ api = HfApi(token=TOKEN)
76
+ api.create_repo(REPO, repo_type="dataset", private=True, exist_ok=True)
77
+ tmp = Path("ds"); tmp.mkdir(exist_ok=True)
78
+ rows = []
79
+ for i, (src, cap) in enumerate(zip(images, captions)):
80
+ fn = f"{i:03d}{Path(src).suffix.lower()}"
81
+ (tmp / fn).write_bytes(Path(src).read_bytes())
82
+ rows.append({"file_name": fn, "prompt": cap})
83
+ (tmp / "metadata.jsonl").write_text("\n".join(json.dumps(r) for r in rows))
84
+ api.upload_folder(repo_id=REPO, repo_type="dataset", folder_path=str(tmp))
85
+ print("dataset:", REPO)
86
+ ```
87
+
88
+ No captions yet? Use the trigger alone as every caption β€” fine for a single-concept style.
89
+
90
+ ## Step 2 β€” launch training (pick ONE)
91
+
92
+ ### Option A β€” run the job yourself (recommended)
93
+ Submit the job on HF Jobs under the user's token:
94
+
95
+ ```bash
96
+ export HF_TOKEN=hf_... # the user's write token
97
+ curl -sO https://__HOST__/train_job.py
98
+ hf jobs uv run --flavor rtx-pro-6000 --timeout 1h -s HF_TOKEN=$HF_TOKEN train_job.py -- \
99
+ --dataset <user>/<lora_name>-dataset \
100
+ --lora-name <lora_name> \
101
+ --trigger "<trigger>" \
102
+ --concept style
103
+ ```
104
+
105
+ Python equivalent:
106
+
107
+ ```python
108
+ from huggingface_hub import HfApi
109
+ api = HfApi(token=TOKEN)
110
+ job = api.run_uv_job(
111
+ "https://__HOST__/train_job.py",
112
+ flavor="rtx-pro-6000", timeout="1h",
113
+ secrets={"HF_TOKEN": TOKEN},
114
+ script_args=["--dataset", "<user>/<lora_name>-dataset",
115
+ "--lora-name", "<lora_name>",
116
+ "--trigger", "<trigger>", "--concept", "style"],
117
+ )
118
+ print(job.url)
119
+ ```
120
+
121
+ Useful flags: `--steps` (1000), `--rank` (32), `--resolution` (1024), `--learning-rate` (3e-4),
122
+ `--quantization {none,fp8,4bit}`, `--no-gallery`, `--num-gallery` (3). Run with `--help` for all.
123
+
124
+ ### Option B β€” let this Space submit the job for you
125
+ POST the user's token + the dataset repo; the Space submits the job (billed to that token):
126
+
127
+ ```bash
128
+ curl -sX POST https://__HOST__/api/train -H 'Content-Type: application/json' -d '{
129
+ "hf_token": "hf_...",
130
+ "dataset_repo": "<user>/<lora_name>-dataset",
131
+ "lora_name": "<lora_name>",
132
+ "trigger": "<trigger>",
133
+ "concept": "style"
134
+ }'
135
+ # -> {"job_id": "...", "url": "https://huggingface.co/jobs/...", "hub_model_id": "<user>/<lora_name>"}
136
+ ```
137
+
138
+ ## Result
139
+ ~40 min on `rtx-pro-6000` (1000 steps, regional torch.compile). The LoRA is pushed to
140
+ `<user>/<lora_name>` with a preview gallery + README. Use it:
141
+
142
+ ```python
143
+ import torch
144
+ from diffusers import Krea2Pipeline
145
+ pipe = Krea2Pipeline.from_pretrained("krea/Krea-2-Turbo", torch_dtype=torch.bfloat16).to("cuda")
146
+ pipe.load_lora_weights("<user>/<lora_name>")
147
+ image = pipe("<trigger>, a fox in a snowy forest", num_inference_steps=8, guidance_scale=0.0).images[0]
148
+ image.save("out.png")
149
+ ```
150
+ '''
151
+
152
+
153
+ def build_agent_doc() -> str:
154
+ return _AGENT_DOC.replace("__HOST__", SPACE_HOST)
155
+
156
  CSS = """
157
  #main_title{text-align:center}
158
  #main_title h1{font-size:2.25rem;margin-bottom:0}
 
162
  .login_logout{width:100% !important}
163
  #login{width:100% !important;margin:0 auto}
164
  #signin_banner{text-align:center}
165
+ .agent_hint{text-align:center;opacity:.7;font-size:.9em;margin:.4em 0 -.2em}
166
+ .agent_curl{max-width:380px;margin:0 auto}
167
  #main_ui.locked{pointer-events:none;opacity:.55;filter:grayscale(.3)}
168
 
169
  /* captioning panel */
 
410
  gr.Column(scale=2, min_width=0)
411
  with gr.Column(scale=0, min_width=380):
412
  gr.LoginButton(elem_id="login", elem_classes=["login_logout"])
413
+ gr.Markdown("…or hand it to your coding agent:", elem_classes=["agent_hint"])
414
+ gr.Code(f"curl {AGENT_URL}", language="shell", interactive=False,
415
+ show_label=False, elem_classes=["agent_curl"])
416
  gr.Column(scale=2, min_width=0)
417
  banner = gr.Markdown(elem_id="signin_banner")
418
 
 
576
 
577
 
578
  if __name__ == "__main__":
579
+ import uvicorn
580
+ from fastapi import FastAPI, Request
581
+ from fastapi.responses import JSONResponse, PlainTextResponse
582
+
583
+ fastapi_app = FastAPI()
584
+
585
+ @fastapi_app.get("/agent", response_class=PlainTextResponse)
586
+ def _agent_doc():
587
+ return build_agent_doc()
588
+
589
+ @fastapi_app.get("/train_job.py", response_class=PlainTextResponse)
590
+ def _train_job():
591
+ return jobs.build_agent_job_script()
592
+
593
+ @fastapi_app.post("/api/train")
594
+ async def _api_train(request: Request):
595
+ try:
596
+ payload = await request.json()
597
+ except Exception: # noqa: BLE001
598
+ return JSONResponse({"error": "invalid JSON body"}, status_code=400)
599
+ try:
600
+ return JSONResponse(jobs.submit_from_dataset(payload))
601
+ except Exception as e: # noqa: BLE001
602
+ return JSONResponse({"error": str(e)}, status_code=400)
603
+
604
+ demo.queue(default_concurrency_limit=4)
605
+ app = gr.mount_gradio_app(fastapi_app, demo, path="/", theme=THEME, css=CSS)
606
+ uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("GRADIO_SERVER_PORT", 7860)))