Trains a LoRA for Krea 2 via fal.ai's fal-ai/krea-2-trainer
API, downloads the result, and converts it to the layer names ComfyUI expects
(via a vendored copy of ComfyUI-Pixaroma's _krea_lora_convert_helpers.py).
Pipeline for one job:
input/<set>/*.png (+*.txt captions)
│ zip + upload
▼
fal.ai queue (submit → poll by request_id → result)
│ download lora_file
▼
outputs/<trigger_phrase>/<name>.safetensors (as returned by fal.ai)
outputs/<trigger_phrase>/<name>_converted.safetensors (renamed keys for ComfyUI)
The web UI shows a connection indicator (green/red dot + username + credit
balance) at the top, a "Reconnect" button, and a way to paste in an API key
without touching .env - see Connecting to fal.ai.
The project is deliberately split into layers so it can run standalone or be mounted inside another (monolith) web backend:
app/core/— all business logic, no FastAPI and no hardcoded disk paths:training_service.py—TrainingService, the job state machine (create_job,advance(job_id),list_jobs,cancel_job). This is what a monolith imports directly, without going through HTTP.job_store.py—JobStore(Protocol) +JsonFileJobStorefor standalone mode. A monolith plugs in its own DB-backed implementation with the same three methods.storage.py—Storage(Protocol) +LocalStorage(theinput//outputs/folders on disk). A monolith can swap in its own storage (S3, etc.).fal_gateway.py— a thin wrapper around the officialfal_clientpackage (upload/submit/poll/result/cancel), pluscheck_connection()(calls fal.ai's account/billing API to verify the key and read back username + credit balance) andset_api_key()to swap credentials at runtime.settings_store.py—SettingsStore, a tiny JSON file that persists an API key entered through the UI (data/settings.json) so it survives a restart. Optional: a monolith normally wouldn't pass one in, since it manages its own secrets.converter.py— callsconvert_file()from the vendored helpers.models.py—Job,TrainingParams,JobStatus— plain dataclasses, no pydantic/FastAPI dependency.
app/api/— the HTTP layer:routes.py::build_router(service)returns a plainAPIRouter. A monolith doesmonolith_app.include_router(build_router(shared_service), prefix="/krea2-trainer")and gets the same endpoints without a second FastAPI app.app.py::create_app(service, poll_interval_seconds=...)— the standalone app: same routes + the web UI's static files + the background poller.
app/worker/poller.py— a simple asyncio loop that callsservice.advance_all()everyKREA2_POLL_INTERVAL_SECONDSseconds. Only needed standalone; a monolith would typically advance jobs from its own task queue (Celery/RQ/cron/webhook) by callingservice.advance(job_id)itself.app/vendor/krea_lora_convert_helpers.py— an exact copy ofComfyUI-Pixaroma's_krea_lora_convert_helpers.py(stdlib-only, notorch/comfyimports), so this app does not depend on a ComfyUI install being present. SetKREA2_HELPERS_PATHto point at a live ComfyUI checkout instead, if you want a single source of truth for the mapping tables.app/web/static/— a plain vanilla HTML/CSS/JS UI for standalone mode.
PENDING → PACKAGING → QUEUED → IN_PROGRESS → DOWNLOADING → CONVERTING → DONE
↘ FAILED / CANCELLED (from any state)
TrainingService.advance(job_id) performs exactly one bounded step and
persists progress through JobStore, so it is safe to call repeatedly
(on a timer, or from an external task queue).
python -m venv .venv
./.venv/Scripts/pip install -r requirements.txt # Windows
# .venv/bin/pip install -r requirements.txt # macOS/Linux
cp .env.example .env
# fill in FAL_KEY in .env (https://fal.ai/dashboard/keys)Run:
./.venv/Scripts/python -m app.mainOpen http://127.0.0.1:8001 for the web UI. The REST API lives under
/api/*, with interactive docs at /docs.
The top of the web UI shows a live connection indicator:
- 🟢 green — the key works; shows the account's username and credit balance when available.
- 🟡 yellow — the key is valid but fal.ai returned an error reading extra info (see below).
- 🔴 red — no working key (missing/invalid, or a network error).
Note: the username/balance readout comes from fal.ai's
GET /v1/account/billing endpoint, which the docs mark as requiring an
admin-scoped key. A regular API key still trains LoRAs fine, but the status
bar may show "connected" without a balance and a permission warning - that's
expected, not a bug.
You can paste a different key straight into the UI ("Set API key" → "Save &
connect") instead of editing .env; it's persisted to data/settings.json
and takes precedence over FAL_KEY on the next restart. "Reconnect" just
re-runs the same check against whichever key is currently active (useful
after a transient network error, or to refresh the balance).
- Drop images into
input/<set_name>/(PNG/JPG/JPEG/WebP), optionally with a same-stemimage.txtcaption next to eachimage.png. Or upload files straight from the web UI ("Image set" section) — it creates/fills the folder underinput/for you. - Enter a trigger phrase and parameters (
auto_captioning,steps,learning_rate,resolution) and start the training job. - The background poller zips the images, uploads them to fal.ai, polls the
job status by
request_id, and once it's done, downloads the.safetensorsfile and converts it. - Results land in
outputs/<trigger_phrase>/:<name>.safetensors— as returned by fal.ai (for diffusers-style loaders);<name>_converted.safetensors— with renamed keys for ComfyUI.
| Method | Path | Description |
|---|---|---|
| GET | /api/fal/status |
Connection indicator: verifies the active key, returns username + credit balance |
| POST | /api/fal/reconnect |
Re-run the same check (no body) |
| POST | /api/fal/key |
Set/replace the active fal.ai API key ({"api_key": "..."}), persisted for standalone mode |
| GET | /api/input-sets |
List folders under input/ with image counts |
| POST | /api/input-sets/{name}/images |
Upload files (multipart) into input/{name}/ |
| POST | /api/trainings |
Create a training job |
| GET | /api/trainings |
List jobs |
| GET | /api/trainings/{id} |
Get one job's status |
| POST | /api/trainings/{id}/cancel |
Cancel a job (and on fal.ai's side, if not finished yet) |
| POST | /api/trainings/{id}/advance |
Manually step the pipeline once (when nothing is running the background poller) |
| GET | /api/trainings/{id}/download?variant=raw|converted |
Download a .safetensors file |
POST /api/trainings body:
{
"input_set": "my_character",
"trigger_phrase": "ohwx_character",
"auto_captioning": "Object/Character",
"steps": 100,
"learning_rate": 0.0005,
"resolution": 768
}Don't use app.api.app.create_app — build a TrainingService with your own
JobStore/Storage implementations (e.g. backed by your DB and object
storage) and mount the router:
from app.api.routes import build_router
from app.core.training_service import TrainingService
service = TrainingService(my_job_store, my_storage, my_fal_gateway, ...)
monolith_app.include_router(build_router(service), prefix="/krea2-trainer")The monolith then decides what advances the pipeline — a periodic task
calling service.advance_all(), a fal.ai webhook handler (pass
fal_webhook to submit, then call _submit/_poll-equivalent logic when
the webhook fires instead of polling), or a manual service.advance(job_id)
call from a task queue.
./.venv/Scripts/pip install -r requirements-dev.txt
./.venv/Scripts/python -m pytestTests cover the converter (app/core/converter.py, against synthetic
.safetensors files), the filesystem storage (app/core/storage.py), and
the full job lifecycle in TrainingService with a fake FalGateway (no
real calls to fal.ai).
JsonFileJobStoreis sized for standalone use (human/poll-rate writes), not high write concurrency — swap in a DB-backedJobStorefor a monolith.- Cancellation on fal.ai's side is best-effort: if the request is already
running, cancelling may not stop the actual computation on fal.ai, but
the job is still marked
cancelledlocally and stops being advanced. - fal.ai's
auto_captioning="Custom"supports a custom captioning prompt; the current UI/schema doesn't expose that field separately — addauto_captioning_prompttoTrainingParams.to_fal_argumentsif you need it.
MIT - see LICENSE. app/vendor/krea_lora_convert_helpers.py is
vendored from the separately MIT-licensed ComfyUI-Pixaroma project; its
original copyright notice is kept intact in
THIRD_PARTY_NOTICES.md.