Skip to content

Commit b2fa006

Browse files
authored
Merge pull request #13 from inlineresearch/z-image-trainer
Z image lora trainer
2 parents 4505f6d + 672c333 commit b2fa006

51 files changed

Lines changed: 4729 additions & 104 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
22

README.md

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
<h3 align="center">AI filmmaking on a node canvas</h3>
44

5-
<p align="center">Inline Studio is a free, open-source app for AI filmmakers. Build a whole visual pipeline on a free-form node canvas, from moodboard to final cut, with local diffusion models (the built-in Inline Core engine) and hosted fal models. Every render is kept as a versioned, non-destructive take.</p>
5+
<p align="center">Inline Studio is a free, open-source app for AI filmmakers. Build a whole visual pipeline on a free-form node canvas, from moodboard to final cut, with local diffusion models (the built-in Inline Core engine) and hosted fal models. Train your own LoRAs on the same canvas. Every render is kept as a versioned, non-destructive take.</p>
66

77
<p align="center">
88
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge"></a>
99
<a href="https://www.python.org/downloads/"><img alt="Python 3.11+" src="https://img.shields.io/badge/Python-3.11%2B-blue?style=for-the-badge&logo=python&logoColor=white"></a>
10-
<a href="../../releases/latest"><img alt="Latest release" src="https://img.shields.io/badge/Release-v1.2.31-blue?style=for-the-badge"></a>
10+
<a href="../../releases/latest"><img alt="Latest release" src="https://img.shields.io/badge/Release-v1.2.41-blue?style=for-the-badge"></a>
1111
<a href="https://discord.gg/cSUS88VdY9"><img alt="Join our Discord" src="https://img.shields.io/badge/Discord-Join%20the%20community-5865F2?logo=discord&logoColor=white&style=for-the-badge"></a>
1212
</p>
1313

@@ -35,6 +35,7 @@ It runs as a **single process on one port**: the Inline Core engine (Python) ser
3535
- **Chain frames into a generative pipeline** - wire one frame's output into the next frame's input. Refine a shot, feed it forward, regenerate the source, and everything downstream follows.
3636
- **Video editing on the canvas** - the **Video Director node** is a timeline-in-a-node that assembles your rendered frames into a single cut, with layered audio (the videos' own audio plus your own music/VO), per-input and per-layer volume, an in-node preview to scrub, and high-res export; the **Trim Video/Audio node** lets you drop in a clip, drag the in/out handles over its filmstrip/waveform, and pass just the trimmed segment downstream.
3737
- **Local generation, built in** - the Inline Core engine runs diffusion models on your own GPU. Z-Image Turbo from a single model file, no external server to set up.
38+
- **Train your own LoRAs** - the Trainer tab is a second canvas where the dataset, captioning, training run, and loss curve are all nodes. The finished LoRA drops into `models/loras/` and shows up in the LoRA loader node, ready to generate with. See [LoRA training](#lora-training).
3839
- **API Nodes for hosted models** - run closed models right on the canvas with no GPU. Add a Generate node, pick a model, and bring your own provider key. See [API Nodes](#api-nodes).
3940
- **Community extensions** - install custom nodes from a GitHub repo in one click, security-reviewed and dependency-isolated. Browse the [registry](https://github.com/inlineresearch/Inline-Registry) or [build your own](https://github.com/inlineresearch/Inline-Studio-Extension-Guide).
4041
- **Free & open source (MIT)** - one process (Python + a browser); runs on macOS, Windows, and Linux.
@@ -228,6 +229,68 @@ The friendly `webui.sh` launcher (in `core/`) maps flags onto the engine's `INLI
228229

229230
`webui.sh` also has `--install` / `--extra NAME` to set up the venv. New to Inline Studio? The [Getting Started guide](https://inlinestudio.art/getting-started) walks you through your first render.
230231

232+
## LoRA training
233+
234+
Train a LoRA on your own images without leaving the app. The **Trainer** tab is a second canvas: wire up the nodes, press Start, and watch it run. When the run finishes, the `.safetensors` lands in `models/loras/`, where the LoRA loader node picks it up automatically, so you can generate with it over in the Studio tab straight away.
235+
236+
![Inline Studio Trainer tab showing the LoRA training node graph with a dataset, live logs, and a loss curve](https://raw.githubusercontent.com/inlineresearch/Inline-Studio/main/screenshots/lora-trainer.png)
237+
238+
### The graph
239+
240+
Five nodes, wired left to right:
241+
242+
- **Load Dataset** picks a training dataset and feeds it downstream. The node face stays a preview (thumbnails, image and caption counts); the images and captions themselves are edited in the side panel.
243+
- **Caption** runs a local captioner over the images that need one, with per-image progress. Captions stay editable afterwards, and a wired dataset overrides the node's own picker.
244+
- **Train LoRA** runs the job. Hyperparameters live behind the Adjust button, off the node face, so the node stays a status surface: a live step counter, the trainer's streaming logs, and a progress bar. The run control is a single chip that reads Start, Stop, or Resume depending on where the run is.
245+
- **Graph** plots the loss curve for whichever run is wired into it, with loss values on the y axis and the step range on the x axis.
246+
- **Resources** is a read-only readout of CPU, RAM, and VRAM as circular gauges. It takes no connections, and you can drop it on the Studio canvas too.
247+
248+
<details>
249+
<summary><b>Datasets, resuming, trigger words, and base model modes</b></summary>
250+
251+
### Datasets and outputs
252+
253+
The sidebar has two tabs. **Datasets** is where you create a dataset, give it a trigger word, add images (drag and drop from your file manager works), and edit captions. **Outputs** lists what training has produced: finished LoRAs with their rank, step count, and resolution, plus any run that stopped early, each with a Resume button.
254+
255+
### Stop and resume
256+
257+
Stopping a run flushes a checkpoint before the process exits, so Resume continues from the step it left off instead of starting over. A checkpoint holds the adapter weights, the optimiser state, the RNG state, and the step number, which is what makes a resumed run a continuation rather than a restart. Runs cut short by a crash or a server restart are recovered the same way and show up under Outputs ready to resume.
258+
259+
### Trigger words
260+
261+
A dataset's trigger word is prepended to every caption during training, so the model sees captions in the form `mytoken, a photo of ...`. Put the same token at the front of your prompt to pull the LoRA in. It is worth matching the phrasing of your captions too: if they all say "an oil painting of", a prompt written the same way will hit the trained style far more reliably than the trigger word alone.
262+
263+
### Base model modes
264+
265+
Z-Image Turbo is step-distilled, and training directly on a distilled model breaks the distillation down (turbo drift). There are two ways around it:
266+
267+
- **Turbo + training adapter** fuses a de-distillation adapter into the base for the duration of training and drops it when the LoRA is saved, which preserves the 8-step speed. Put [ostris/zimage_turbo_training_adapter](https://huggingface.co/ostris/zimage_turbo_training_adapter) in `models/loras/`; any filename containing `adapter` is detected automatically, or point `INLINE_ZIMAGE_TRAIN_ADAPTER` at a specific file. Keep runs short, since the adapter slows the breakdown rather than preventing it.
268+
- **De-Turbo** trains without an adapter and needs no extra download.
269+
270+
</details>
271+
272+
### Install
273+
274+
The trainer's dependencies (PEFT, 8-bit Adam, the captioner) sit behind the `training` extra, so a normal install stays lean:
275+
276+
```bash
277+
cd core
278+
./webui.sh --install --extra runtime --extra training
279+
```
280+
281+
Weights are bring-your-own, the same as generation: nothing is downloaded behind your back. Training reuses the Z-Image files you already have in `models/diffusion_models/`, `models/vae/`, and `models/text_encoders/`. The captioner is the one exception, fetched once into the Hugging Face cache the first time you press Auto-caption.
282+
283+
### Hardware
284+
285+
Training needs a real GPU, and the resolution you train at drives peak VRAM far more than rank or batch size does. Two configurations have been run end to end:
286+
287+
| GPU | Resolution | Notes |
288+
| --------------- | ---------- | -------------------------------------------------------------------- |
289+
| 16GB (Tesla T4) | 512px | Peaks around 13GB. 768 and 1024 both run out of memory on this card. |
290+
| 24GB (L4) | 1024px | Run with the Turbo training adapter fused in. |
291+
292+
To keep the peak down, the VAE and text encoder are loaded first, used to cache latents and captions, then freed before the transformer loads, so the peak is the transformer on its own rather than all three resident at once. If you do hit an out-of-memory error, lower the training resolution before changing anything else.
293+
231294
## FAQ
232295

233296
### Is Inline Studio free?
@@ -242,10 +305,6 @@ Only for **local** generation. The built-in Inline Core engine renders on the GP
242305

243306
See [Two ways to generate](#two-ways-to-generate): local Z-Image on your own GPU, or hosted fal models. Adding a new local model is a Core change (a model runner), no UI release.
244307

245-
### Does it use ComfyUI?
246-
247-
No. Built-in generation is all Inline Core (local, on your own GPU) plus hosted fal models — nothing external to stand up. There's no ComfyUI connection, Generate tab, or workflow linking.
248-
249308
## Contributing
250309

251310
Inline Studio is early and moving fast, any issues, ideas, and pull requests are all welcome. If you're poking at the code, [CLAUDE.md](CLAUDE.md) is the engineering guide: it explains the architecture, the data model, and the conventions to follow.
@@ -256,6 +315,8 @@ Want to help by using it for real? Try the [creator task](task.md): build a shor
256315

257316
Inline Core's multi-GPU denoise builds on [**xDiT**](https://github.com/xdit-project/xDiT)'s PipeFusion and Ulysses parallelism.
258317

318+
The LoRA trainer's approach to training on a step-distilled model follows [**ai-toolkit**](https://github.com/ostris/ai-toolkit) by ostris, and Turbo mode uses his [Z-Image Turbo training adapter](https://huggingface.co/ostris/zimage_turbo_training_adapter).
319+
259320
## Help shape Inline Studio
260321

261322
Are you an AI filmmaker who wants to help us make this better? We run a **paid trial feedback program**: use Inline Studio on real work, tell us what helps and what gets in your way, and get paid for your time.

core/pyproject.toml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
# PyPI name; the import package is `inline_core` (src/inline_core).
33
name = "inline-core"
4-
version = "1.2.31"
4+
version = "1.2.41"
55
description = "The generation engine behind Inline Studio."
66
readme = "README.md"
77
requires-python = ">=3.11"
@@ -39,6 +39,20 @@ parallel = [
3939
"xfuser>=0.4",
4040
"nvidia-ml-py>=12",
4141
]
42+
# LoRA training (the Trainer tab): PEFT adapter training + local auto-caption + host/GPU telemetry.
43+
# Install alongside `runtime`: `.[runtime,training]`. Reuses runtime's torch/diffusers/accelerate.
44+
training = [
45+
"peft>=0.11",
46+
# 8-bit Adam keeps optimizer state small; no macOS wheels, so skip it there (AdamW fallback).
47+
"bitsandbytes>=0.43; platform_system != 'Darwin'",
48+
"Pillow>=10",
49+
# Telemetry (CPU/RAM + per-GPU NVML) for the Trainer tab.
50+
"psutil>=5.9",
51+
"nvidia-ml-py>=12",
52+
# Florence-2 auto-captioner is loaded via trust_remote_code and needs these.
53+
"timm>=1.0",
54+
"einops>=0.7",
55+
]
4256
dev = [
4357
"pytest>=8",
4458
"ruff>=0.6",

core/src/inline_core/server/app.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from ..models.catalog import ModelCatalog
3030
from ..models.requirements import RequirementsRegistry
3131
from ..runtime.file_store import FileTakeStore
32+
from ..studio.system_stats import SystemStats
3233
from .assets import AssetStore
3334
from .manager import RunConflict, RunManager
3435
from .rpc import EventBroadcaster, RpcRouter
@@ -139,13 +140,19 @@ def create_app(
139140
manager = RunManager(registry, cache, policy, store=run_store, takes=FileTakeStore(takes_root))
140141
rpc = rpc or RpcRouter()
141142
events = events or EventBroadcaster()
143+
# Host/GPU telemetry for the Trainer tab; only meaningful with the SPA (studio) backend wired.
144+
stats = SystemStats(events) if studio_store is not None else None
142145

143146
@asynccontextmanager
144147
async def lifespan(app: FastAPI): # noqa: ANN202
145148
manager.bind_loop(asyncio.get_running_loop())
146149
catalog.ensure_dirs()
147150
catalog.scan()
151+
if stats is not None:
152+
stats.start()
148153
yield
154+
if stats is not None:
155+
stats.stop()
149156
manager.shutdown()
150157

151158
app = FastAPI(title="Inline Core", version="0.0.0", lifespan=lifespan)
@@ -298,6 +305,7 @@ async def studio_events(websocket: WebSocket) -> None:
298305
from ..studio.handlers import register_studio_handlers
299306
from ..studio.models import ModelDownloads
300307
from ..studio.timeline.render import Timeline
308+
from ..studio.training import Training
301309

302310
def core_models() -> dict[str, Any]:
303311
return {
@@ -316,6 +324,7 @@ def core_status() -> dict[str, Any]:
316324
generation=CoreGeneration(studio_store, manager, events),
317325
fal_generation=FalGeneration(studio_store, events),
318326
timeline=Timeline(studio_store, events),
327+
training=Training(studio_store, events, on_output=catalog.rescan),
319328
# Explicit model downloads write into models/; rescan so new files bump the registry.
320329
# The policy lets the requirements popup show a memory fit estimate before a load;
321330
# the requirements registry says which node types have models at all.

core/src/inline_core/server/rpc.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ class EventBroadcaster:
5656
def __init__(self) -> None:
5757
self._subscribers: set[asyncio.Queue[dict[str, Any]]] = set()
5858

59+
@property
60+
def subscriber_count(self) -> int:
61+
"""Open ``/events`` sockets - lets pollers (telemetry) skip work when nobody listens."""
62+
return len(self._subscribers)
63+
5964
def add(self) -> asyncio.Queue[dict[str, Any]]:
6065
queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
6166
self._subscribers.add(queue)

core/src/inline_core/studio/handlers.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def register_studio_handlers(
3939
generation: Any = None,
4040
fal_generation: Any = None,
4141
timeline: Any = None,
42+
training: Any = None,
4243
model_downloads: Any = None,
4344
app_version: str = "1.0.0",
4445
) -> None:
@@ -155,7 +156,9 @@ def delete_take(take_id: str) -> None:
155156
reg("frames:deleteTake", delete_take)
156157

157158
# --- moodboard ------------------------------------------------------------------------------
158-
reg("moodboard:list", lambda: mb.list_board(conn()))
159+
# `surface` defaults to the Studio moodboard so existing callers are unchanged; the Trainer tab
160+
# passes "trainer" to get its own isolated canvas out of the same tables.
161+
reg("moodboard:list", lambda surface=mb.STUDIO_SURFACE: mb.list_board(conn(), surface))
159162
reg("moodboard:addAsset", lambda aid, x, y: mb.add_asset(conn(), aid, x, y))
160163
reg("moodboard:addText", lambda x, y: mb.add_text(conn(), x, y))
161164
reg("moodboard:addFrameFromAsset", lambda aid, x, y: mb.add_frame_from_asset(conn(), aid, x, y))
@@ -183,7 +186,18 @@ def delete_take(take_id: str) -> None:
183186
reg("moodboard:setConnectorVolume", lambda cid, vol: mb.set_connector_volume(conn(), cid, vol))
184187
reg(
185188
"moodboard:replaceBoard",
186-
lambda items, connectors: mb.replace_board(conn(), items, connectors),
189+
lambda items, connectors, surface=mb.STUDIO_SURFACE: mb.replace_board(
190+
conn(), items, connectors, surface
191+
),
192+
)
193+
# Trainer-canvas nodes (plus the shared read-only resource node, which either canvas can host).
194+
reg("moodboard:addTrainDataset", lambda x, y: mb.add_train_dataset(conn(), x, y))
195+
reg("moodboard:addCaption", lambda x, y: mb.add_caption(conn(), x, y))
196+
reg("moodboard:addTrainer", lambda x, y: mb.add_trainer(conn(), x, y))
197+
reg("moodboard:addLossGraph", lambda x, y: mb.add_loss_graph(conn(), x, y))
198+
reg(
199+
"moodboard:addResource",
200+
lambda x, y, surface=mb.STUDIO_SURFACE: mb.add_resource(conn(), x, y, surface),
187201
)
188202

189203
# --- generation -----------------------------------------------------------------------------
@@ -207,6 +221,25 @@ def cancel_generation(frame_id: str | None = None) -> None:
207221
reg("generation:cancel", cancel_generation)
208222
reg("generation:resumePending", lambda: None)
209223

224+
# --- LoRA training (dataset CRUD + the training run subprocess) ------------------------------
225+
if training is not None:
226+
reg("training:listDatasets", lambda: training.list_datasets())
227+
reg("training:createDataset", lambda inp: training.create_dataset(inp))
228+
reg("training:listItems", lambda did: training.list_items(did))
229+
reg("training:addItems", lambda did, aids: training.add_items(did, aids))
230+
reg("training:removeItem", lambda iid: training.remove_item(iid))
231+
reg("training:setCaption", lambda iid, cap: training.set_caption(iid, cap))
232+
reg("training:autoCaption", lambda did, overwrite: training.auto_caption(did, overwrite))
233+
reg("training:listRuns", lambda: training.list_runs())
234+
reg("training:start", lambda did, hp: training.start(did, hp))
235+
reg("training:resume", lambda rid: training.resume(rid))
236+
reg("training:cancel", lambda rid: training.cancel(rid))
237+
reg("training:status", lambda rid: training.status(rid))
238+
else:
239+
for ch in ("listDatasets", "createDataset", "listItems", "addItems", "removeItem",
240+
"setCaption", "autoCaption", "listRuns", "start", "resume", "cancel", "status"):
241+
reg(f"training:{ch}", not_wired("LoRA training"))
242+
210243
# --- fal settings (key stored server-side) --------------------------------------------------
211244
reg("falSettings:status", store.fal_status)
212245
reg("falSettings:setApiKey", store.set_fal_key)

0 commit comments

Comments
 (0)