Fix Z-Image image-gen + LoRA training; add separate LoRA training base model

- images.py: stop reusing cached prompt embeds for Z-Image. Its encode_prompt
  returns per-sample lists the pipeline consumes during a run, so cache-HIT
  reuse corrupted the batch (AssertionError: len(size) == bsz in unpatchify).
  Z-Image now always encodes natively; cache key also bound to id(pipeline).
- loras.py: fix wrong import (multi_model_manager lives in codai.models.manager,
  not codai.api.state) used for VRAM unload + base-model path resolution; add an
  up-front architecture guard so transformer/DiT models (Z-Image/Flux/SD3) fail
  with a clear message instead of crashing in the CLIP tokenizer.
- New train_base_model on LoraTrainRequest: train LoRAs against a separate
  UNet-based SD1.x/SDXL model while generation keeps using the DiT image model.
- gen_township_fighters.py: thread --lora-train-base-model / web field through
  train_lora, _train_profile_loras, stage_loras/stage_env_loras, all run + per-
  card call sites; CONFIG_FIELDS + live default_args apply on /start, /save-config.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 2c736da4
...@@ -737,11 +737,23 @@ async def _generate_with_diffusers(pipeline, request, global_args, http_request= ...@@ -737,11 +737,23 @@ async def _generate_with_diffusers(pipeline, request, global_args, http_request=
# Try to encode the prompt once and reuse the embeddings. # Try to encode the prompt once and reuse the embeddings.
# Falls back to passing the plain text prompt if encoding fails. # Falls back to passing the plain text prompt if encoding fails.
# ------------------------------------------------------------------ # ------------------------------------------------------------------
model_id = getattr(pipeline, 'model_name_or_path', None) or str(type(pipeline).__name__) # Bind the cache entry to this specific *loaded* pipeline instance, so a
# reload (the model-swapping workflow does this constantly) is a guaranteed
# cache miss instead of feeding stale GPU tensors into a fresh pipeline.
_pipe_cls = str(type(pipeline).__name__)
_model_name = getattr(pipeline, 'model_name_or_path', None) or _pipe_cls
model_id = f"{_model_name}#{id(pipeline)}"
neg_prompt = getattr(request, 'negative_prompt', None) or "" neg_prompt = getattr(request, 'negative_prompt', None) or ""
do_cfg = cfg_scale > 1.0 do_cfg = cfg_scale > 1.0
cached_embeds = _embed_cache.get(request.prompt, neg_prompt, model_id) # Some pipelines (Z-Image) return prompt embeddings as *per-sample lists*
# rather than stacked tensors, and the pipeline consumes/extends those
# lists during a run. Reusing them on a later request corrupts the batch
# dimension (`assert len(size) == bsz` in the Z-Image transformer's
# unpatchify). These are not safe to cache, so encode fresh every time.
_embed_cacheable = "ZImage" not in _pipe_cls
cached_embeds = _embed_cache.get(request.prompt, neg_prompt, model_id) if _embed_cacheable else None
embed_kwargs = {} embed_kwargs = {}
cache_hit = False cache_hit = False
...@@ -749,6 +761,11 @@ async def _generate_with_diffusers(pipeline, request, global_args, http_request= ...@@ -749,6 +761,11 @@ async def _generate_with_diffusers(pipeline, request, global_args, http_request=
embed_kwargs = cached_embeds embed_kwargs = cached_embeds
cache_hit = True cache_hit = True
print(f"Prompt embed cache HIT for model '{model_id}'") print(f"Prompt embed cache HIT for model '{model_id}'")
elif not _embed_cacheable:
# Z-Image et al.: don't pre-encode at all. Leaving embed_kwargs empty
# makes the call below pass the raw prompt, so the pipeline runs its own
# native encode + batching (the only path that survives reuse here).
pass
else: else:
# Try to encode and cache # Try to encode and cache
try: try:
...@@ -781,7 +798,7 @@ async def _generate_with_diffusers(pipeline, request, global_args, http_request= ...@@ -781,7 +798,7 @@ async def _generate_with_diffusers(pipeline, request, global_args, http_request=
'pooled_prompt_embeds': enc[2], 'pooled_prompt_embeds': enc[2],
'negative_pooled_prompt_embeds': enc[3], 'negative_pooled_prompt_embeds': enc[3],
} }
if embed_kwargs: if embed_kwargs and _embed_cacheable:
_embed_cache.put(request.prompt, neg_prompt, model_id, embed_kwargs) _embed_cache.put(request.prompt, neg_prompt, model_id, embed_kwargs)
print(f"Prompt embed cache STORE for model '{model_id}'") print(f"Prompt embed cache STORE for model '{model_id}'")
except Exception as e: except Exception as e:
......
...@@ -131,6 +131,10 @@ def _require_api_auth(request: Request) -> None: ...@@ -131,6 +131,10 @@ def _require_api_auth(request: Request) -> None:
class LoraTrainRequest(BaseModel): class LoraTrainRequest(BaseModel):
name: str # output LoRA name (folder) name: str # output LoRA name (folder)
base_model: str # image model key (models.json) or HF id / path base_model: str # image model key (models.json) or HF id / path
# Optional separate UNet-based SD1.x/SDXL model to train the LoRA against,
# when `base_model` (the generation model) is a transformer/DiT (Z-Image,
# Flux, SD3) this trainer can't target. Falls back to base_model if unset.
train_base_model: Optional[str] = None
character: Optional[str] = None # saved character profile to pull images from character: Optional[str] = None # saved character profile to pull images from
environment: Optional[str] = None # OR saved environment profile to pull images from environment: Optional[str] = None # OR saved environment profile to pull images from
images: Optional[List[str]] = None # OR explicit base64/data-uri images images: Optional[List[str]] = None # OR explicit base64/data-uri images
...@@ -148,7 +152,7 @@ class LoraTrainRequest(BaseModel): ...@@ -148,7 +152,7 @@ class LoraTrainRequest(BaseModel):
def _resolve_base_model_path(base_model: str) -> str: def _resolve_base_model_path(base_model: str) -> str:
"""Resolve an image model key (or path/HF id) to a diffusers model directory.""" """Resolve an image model key (or path/HF id) to a diffusers model directory."""
try: try:
from codai.api.state import multi_model_manager from codai.models.manager import multi_model_manager
for key in (f"image:{base_model}", base_model): for key in (f"image:{base_model}", base_model):
cfg = multi_model_manager.config.get(key) cfg = multi_model_manager.config.get(key)
if cfg: if cfg:
...@@ -222,7 +226,9 @@ def _train_lora_sync(req: LoraTrainRequest) -> dict: ...@@ -222,7 +226,9 @@ def _train_lora_sync(req: LoraTrainRequest) -> dict:
from transformers import CLIPTextModel, CLIPTokenizer from transformers import CLIPTextModel, CLIPTokenizer
name = req.name name = req.name
base_path = _resolve_base_model_path(req.base_model) # Train against a dedicated UNet model when provided (the generation model
# may be a DiT this trainer can't target); otherwise use base_model.
base_path = _resolve_base_model_path(req.train_base_model or req.base_model)
steps = max(50, min(5000, int(req.steps or 800))) steps = max(50, min(5000, int(req.steps or 800)))
rank = max(2, min(128, int(req.rank or 16))) rank = max(2, min(128, int(req.rank or 16)))
resolution = int(req.resolution or 512) resolution = int(req.resolution or 512)
...@@ -247,7 +253,7 @@ def _train_lora_sync(req: LoraTrainRequest) -> dict: ...@@ -247,7 +253,7 @@ def _train_lora_sync(req: LoraTrainRequest) -> dict:
# Free VRAM: evict every resident model so training has the whole GPU. # Free VRAM: evict every resident model so training has the whole GPU.
try: try:
from codai.api.state import multi_model_manager from codai.models.manager import multi_model_manager
multi_model_manager.unload_all_models() multi_model_manager.unload_all_models()
except Exception as e: except Exception as e:
print(f" [lora] could not unload models before training: {e}") print(f" [lora] could not unload models before training: {e}")
...@@ -257,6 +263,22 @@ def _train_lora_sync(req: LoraTrainRequest) -> dict: ...@@ -257,6 +263,22 @@ def _train_lora_sync(req: LoraTrainRequest) -> dict:
_set_progress(status="preparing", message=f"loading base model: {base_path}") _set_progress(status="preparing", message=f"loading base model: {base_path}")
# This DreamBooth-LoRA trainer only supports UNet-based SD1.x / SDXL
# pipelines. Transformer/DiT models (Z-Image, Flux, SD3, …) have a
# `transformer/` subfolder and no `unet/`; loading their non-CLIP tokenizer
# as a CLIPTokenizer crashes deep inside transformers. Detect that up front
# and fail with an actionable message instead.
import os as _os
_has_unet = _os.path.isdir(_os.path.join(base_path, "unet"))
_has_transformer = _os.path.isdir(_os.path.join(base_path, "transformer"))
if _has_transformer and not _has_unet:
raise ValueError(
f"LoRA training base model '{base_path}' is a transformer/DiT "
f"architecture (has 'transformer/', no 'unet/'). This trainer only "
f"supports UNet-based SD1.x/SDXL models. Configure an SD1.x or SDXL "
f"image model as the LoRA training base."
)
# Detect SDXL by attempting to load a second tokenizer. # Detect SDXL by attempting to load a second tokenizer.
is_sdxl = False is_sdxl = False
try: try:
...@@ -537,6 +559,7 @@ def _write_meta(name, req, base_path, n_images, arch, instance_prompt): ...@@ -537,6 +559,7 @@ def _write_meta(name, req, base_path, n_images, arch, instance_prompt):
meta = { meta = {
"name": name, "name": name,
"base_model": req.base_model, "base_model": req.base_model,
"train_base_model": req.train_base_model or req.base_model,
"base_path": base_path, "base_path": base_path,
"arch": arch, "arch": arch,
"instance_prompt": instance_prompt, "instance_prompt": instance_prompt,
......
...@@ -695,11 +695,13 @@ class CoderAIClient: ...@@ -695,11 +695,13 @@ class CoderAIClient:
def train_lora(self, name: str, base_model: str, character: str = None, def train_lora(self, name: str, base_model: str, character: str = None,
environment: str = None, images: list = None, environment: str = None, images: list = None,
steps: int = 800, rank: int = 16, steps: int = 800, rank: int = 16,
resolution: int = 512) -> dict: resolution: int = 512, train_base_model: str = None) -> dict:
"""Train a per-character or per-environment LoRA on the server. """Train a per-character or per-environment LoRA on the server.
Blocks until complete.""" Blocks until complete."""
body = {"name": name, "base_model": base_model, body = {"name": name, "base_model": base_model,
"steps": int(steps), "rank": int(rank), "resolution": int(resolution)} "steps": int(steps), "rank": int(rank), "resolution": int(resolution)}
if train_base_model:
body["train_base_model"] = train_base_model
if character: if character:
body["character"] = character body["character"] = character
if environment: if environment:
...@@ -1222,6 +1224,7 @@ CONFIG_FIELDS = [ ...@@ -1222,6 +1224,7 @@ CONFIG_FIELDS = [
"only_prompts", "only_videos", "only_prompts", "only_videos",
"consistency", "keyframe_steps", "keyframe_size", "consistency", "keyframe_steps", "keyframe_size",
"character_strength", "lora_steps", "lora_rank", "lora_weight", "character_strength", "lora_steps", "lora_rank", "lora_weight",
"lora_train_base_model",
"no_env_loras", "env_lora_steps", "env_lora_rank", "env_lora_weight", "no_env_loras", "env_lora_steps", "env_lora_rank", "env_lora_weight",
"web_port", "web_port",
] ]
...@@ -1354,12 +1357,16 @@ _LORA_KINDS = { ...@@ -1354,12 +1357,16 @@ _LORA_KINDS = {
def _train_profile_loras(client: CoderAIClient, image_model: str, out_dir: Path, def _train_profile_loras(client: CoderAIClient, image_model: str, out_dir: Path,
names: list, kind: str, names: list, kind: str,
lora_steps: int = 800, lora_rank: int = 16) -> dict: lora_steps: int = 800, lora_rank: int = 16,
train_base_model: str = None) -> dict:
"""Train one identity LoRA per profile of `kind` (server-side). """Train one identity LoRA per profile of `kind` (server-side).
Returns {name: lora_path}. Resumable: skips profiles whose LoRA already Returns {name: lora_path}. Resumable: skips profiles whose LoRA already
exists locally (<kind>_loras.json) or on the server. All training is grouped exists locally (<kind>_loras.json) or on the server. All training is grouped
here so the image base model is touched once for the whole batch. here so the image base model is touched once for the whole batch.
`train_base_model` overrides the model the LoRA is *trained* against (must be
a UNet-based SD1.x/SDXL model); `image_model` stays the generation model.
""" """
spec = _LORA_KINDS[kind] spec = _LORA_KINDS[kind]
_log("\n" + "═" * 60) _log("\n" + "═" * 60)
...@@ -1400,6 +1407,8 @@ def _train_profile_loras(client: CoderAIClient, image_model: str, out_dir: Path, ...@@ -1400,6 +1407,8 @@ def _train_profile_loras(client: CoderAIClient, image_model: str, out_dir: Path,
f"({lora_steps} steps, rank {lora_rank}) — this can take a while…") f"({lora_steps} steps, rank {lora_rank}) — this can take a while…")
train_kwargs = dict(name=lora_name, base_model=image_model, train_kwargs = dict(name=lora_name, base_model=image_model,
steps=lora_steps, rank=lora_rank) steps=lora_steps, rank=lora_rank)
if train_base_model:
train_kwargs["train_base_model"] = train_base_model
train_kwargs[kind] = name # character=name OR environment=name train_kwargs[kind] = name # character=name OR environment=name
try: try:
res = _run_with_spinner( res = _run_with_spinner(
...@@ -1421,17 +1430,19 @@ def _train_profile_loras(client: CoderAIClient, image_model: str, out_dir: Path, ...@@ -1421,17 +1430,19 @@ def _train_profile_loras(client: CoderAIClient, image_model: str, out_dir: Path,
def stage_loras(client: CoderAIClient, image_model: str, out_dir: Path, def stage_loras(client: CoderAIClient, image_model: str, out_dir: Path,
char_names: list, lora_steps: int = 800, lora_rank: int = 16) -> dict: char_names: list, lora_steps: int = 800, lora_rank: int = 16,
train_base_model: str = None) -> dict:
"""Train one identity LoRA per fighter. Returns {fighter: lora_path}.""" """Train one identity LoRA per fighter. Returns {fighter: lora_path}."""
return _train_profile_loras(client, image_model, out_dir, char_names, return _train_profile_loras(client, image_model, out_dir, char_names,
"character", lora_steps, lora_rank) "character", lora_steps, lora_rank, train_base_model)
def stage_env_loras(client: CoderAIClient, image_model: str, out_dir: Path, def stage_env_loras(client: CoderAIClient, image_model: str, out_dir: Path,
env_names: list, lora_steps: int = 800, lora_rank: int = 16) -> dict: env_names: list, lora_steps: int = 800, lora_rank: int = 16,
train_base_model: str = None) -> dict:
"""Train one identity LoRA per environment. Returns {environment: lora_path}.""" """Train one identity LoRA per environment. Returns {environment: lora_path}."""
return _train_profile_loras(client, image_model, out_dir, env_names, return _train_profile_loras(client, image_model, out_dir, env_names,
"environment", lora_steps, lora_rank) "environment", lora_steps, lora_rank, train_base_model)
def _generate_keyframes(client: CoderAIClient, image_model: str, keyframe_dir: Path, def _generate_keyframes(client: CoderAIClient, image_model: str, keyframe_dir: Path,
...@@ -2208,6 +2219,9 @@ def launch_web_ui(default_args): ...@@ -2208,6 +2219,9 @@ def launch_web_ui(default_args):
try: try:
kwargs = dict(name=lora_name, base_model=model, kwargs = dict(name=lora_name, base_model=model,
steps=int(steps), rank=int(rank)) steps=int(steps), rank=int(rank))
_tbm = getattr(default_args, "lora_train_base_model", None) or None
if _tbm:
kwargs["train_base_model"] = _tbm
kwargs[kind] = name kwargs[kind] = name
result["res"] = client.train_lora(**kwargs) result["res"] = client.train_lora(**kwargs)
except Exception as e: except Exception as e:
...@@ -2717,6 +2731,11 @@ textarea{background:#111;border:1px solid #333;color:#e0e0e0;padding:.35rem .5re ...@@ -2717,6 +2731,11 @@ textarea{background:#111;border:1px solid #333;color:#e0e0e0;padding:.35rem .5re
</div> </div>
</div> </div>
<div id=lora_fields style="margin-top:.6rem"> <div id=lora_fields style="margin-top:.6rem">
<div><label>LoRA training base model <span class=hint>(SD1.x/SDXL — leave empty to train on the image model)</span></label>
<input name=lora_train_base_model type=text style="width:100%"
placeholder="e.g. stabilityai/stable-diffusion-xl-base-1.0"
value="{_v('lora_train_base_model','')}"></div>
<p class=hint style="margin:.2rem 0 .6rem">Z-Image / Flux / SD3 image models can't be LoRA-trained directly. Set a UNet-based SD1.x or SDXL model here to train identity LoRAs while still generating with the image model above.</p>
<label style="margin-top:0">Character LoRAs <span class=hint>(per-fighter identity)</span></label> <label style="margin-top:0">Character LoRAs <span class=hint>(per-fighter identity)</span></label>
<div class=row3> <div class=row3>
<div><label>LoRA train steps</label> <div><label>LoRA train steps</label>
...@@ -4499,6 +4518,7 @@ async function pollJob(){ ...@@ -4499,6 +4518,7 @@ async function pollJob(){
"lora_steps": int(_fv("lora_steps", "800") or 800), "lora_steps": int(_fv("lora_steps", "800") or 800),
"lora_rank": int(_fv("lora_rank", "16") or 16), "lora_rank": int(_fv("lora_rank", "16") or 16),
"lora_weight": float(_fv("lora_weight", "0.85") or 0.85), "lora_weight": float(_fv("lora_weight", "0.85") or 0.85),
"lora_train_base_model": _s(_fv("lora_train_base_model")) or "",
"no_env_loras": "env_loras" not in form, "no_env_loras": "env_loras" not in form,
"env_lora_steps": int(_fv("env_lora_steps", "800") or 800), "env_lora_steps": int(_fv("env_lora_steps", "800") or 800),
"env_lora_rank": int(_fv("env_lora_rank", "16") or 16), "env_lora_rank": int(_fv("env_lora_rank", "16") or 16),
...@@ -4520,7 +4540,7 @@ async function pollJob(){ ...@@ -4520,7 +4540,7 @@ async function pollJob(){
# immediately, so subsequent per-profile jobs (regenerate, train # immediately, so subsequent per-profile jobs (regenerate, train
# LoRA) and runs use them — not the values from script launch. # LoRA) and runs use them — not the values from script launch.
for _k in ("base_url", "api_key", "image_model", for _k in ("base_url", "api_key", "image_model",
"video_model", "text_model"): "video_model", "text_model", "lora_train_base_model"):
setattr(default_args, _k, cfg.get(_k)) setattr(default_args, _k, cfg.get(_k))
_web_log(f" ⚙ Settings applied (image model: " _web_log(f" ⚙ Settings applied (image model: "
f"{cfg.get('image_model') or 'auto'})") f"{cfg.get('image_model') or 'auto'})")
...@@ -4628,6 +4648,7 @@ async function pollJob(){ ...@@ -4628,6 +4648,7 @@ async function pollJob(){
ns.lora_steps = int(_fv("lora_steps", "800")) ns.lora_steps = int(_fv("lora_steps", "800"))
ns.lora_rank = int(_fv("lora_rank", "16")) ns.lora_rank = int(_fv("lora_rank", "16"))
ns.lora_weight = float(_fv("lora_weight", "0.85")) ns.lora_weight = float(_fv("lora_weight", "0.85"))
ns.lora_train_base_model = (_fv("lora_train_base_model", "") or "").strip()
# Environment LoRAs: checkbox "env_loras" present ⇒ train them. # Environment LoRAs: checkbox "env_loras" present ⇒ train them.
ns.no_env_loras = ("env_loras" not in form) ns.no_env_loras = ("env_loras" not in form)
ns.env_lora_steps = int(_fv("env_lora_steps", "800")) ns.env_lora_steps = int(_fv("env_lora_steps", "800"))
...@@ -4658,7 +4679,7 @@ async function pollJob(){ ...@@ -4658,7 +4679,7 @@ async function pollJob(){
# Apply the submitted connection/model settings to the live session # Apply the submitted connection/model settings to the live session
# so later per-profile jobs (regenerate, train LoRA) use them too. # so later per-profile jobs (regenerate, train LoRA) use them too.
for _k in ("base_url", "api_key", "image_model", for _k in ("base_url", "api_key", "image_model",
"video_model", "text_model"): "video_model", "text_model", "lora_train_base_model"):
setattr(default_args, _k, getattr(ns, _k, None)) setattr(default_args, _k, getattr(ns, _k, None))
# Apply the same shortcut logic as CLI # Apply the same shortcut logic as CLI
...@@ -4923,7 +4944,8 @@ async function pollJob(){ ...@@ -4923,7 +4944,8 @@ async function pollJob(){
f"{', '.join(char_names)}") f"{', '.join(char_names)}")
lora_map = stage_loras(client, image_model, out_dir_r, char_names or [], lora_map = stage_loras(client, image_model, out_dir_r, char_names or [],
lora_steps=getattr(args, "lora_steps", 800), lora_steps=getattr(args, "lora_steps", 800),
lora_rank=getattr(args, "lora_rank", 16)) lora_rank=getattr(args, "lora_rank", 16),
train_base_model=getattr(args, "lora_train_base_model", None) or None)
else: else:
_web_log(" ⚠ No characters found to train LoRAs for. Generate or " _web_log(" ⚠ No characters found to train LoRAs for. Generate or "
"select fighters first (Characters page).") "select fighters first (Characters page).")
...@@ -4933,7 +4955,8 @@ async function pollJob(){ ...@@ -4933,7 +4955,8 @@ async function pollJob(){
f"{', '.join(env_names)}") f"{', '.join(env_names)}")
env_lora_map = stage_env_loras(client, image_model, out_dir_r, env_names or [], env_lora_map = stage_env_loras(client, image_model, out_dir_r, env_names or [],
lora_steps=getattr(args, "env_lora_steps", 800), lora_steps=getattr(args, "env_lora_steps", 800),
lora_rank=getattr(args, "env_lora_rank", 16)) lora_rank=getattr(args, "env_lora_rank", 16),
train_base_model=getattr(args, "lora_train_base_model", None) or None)
else: else:
_web_log(" ⚠ No environments found to train LoRAs for.") _web_log(" ⚠ No environments found to train LoRAs for.")
...@@ -5242,6 +5265,11 @@ OUTPUT LAYOUT ...@@ -5242,6 +5265,11 @@ OUTPUT LAYOUT
help="LoRA rank (default: 16).") help="LoRA rank (default: 16).")
cons_grp.add_argument("--lora-weight", type=float, default=0.85, metavar="F", cons_grp.add_argument("--lora-weight", type=float, default=0.85, metavar="F",
help="Weight applied to each character LoRA at generation (default: 0.85).") help="Weight applied to each character LoRA at generation (default: 0.85).")
cons_grp.add_argument("--lora-train-base-model", default="", metavar="MODEL",
help="Separate UNet-based SD1.x/SDXL model (models.json key or HF id/path) "
"to TRAIN LoRAs against, when the generation image model is a "
"transformer/DiT (Z-Image, Flux, SD3) this trainer can't target. "
"Generation still uses --image-model. Empty = train on --image-model.")
cons_grp.add_argument("--no-env-loras", action="store_true", cons_grp.add_argument("--no-env-loras", action="store_true",
help="Do not train/apply per-environment identity LoRAs when the " help="Do not train/apply per-environment identity LoRAs when the "
"'lora' strategy is active (by default environments get LoRAs too).") "'lora' strategy is active (by default environments get LoRAs too).")
...@@ -5393,12 +5421,14 @@ OUTPUT LAYOUT ...@@ -5393,12 +5421,14 @@ OUTPUT LAYOUT
if "lora" in consistency and not args.skip_videos and (char_names or []): if "lora" in consistency and not args.skip_videos and (char_names or []):
lora_map = stage_loras(client, image_model, out_dir, char_names or [], lora_map = stage_loras(client, image_model, out_dir, char_names or [],
lora_steps=getattr(args, "lora_steps", 800), lora_steps=getattr(args, "lora_steps", 800),
lora_rank=getattr(args, "lora_rank", 16)) lora_rank=getattr(args, "lora_rank", 16),
train_base_model=getattr(args, "lora_train_base_model", None) or None)
if ("lora" in consistency and not args.skip_videos if ("lora" in consistency and not args.skip_videos
and not getattr(args, "no_env_loras", False) and (env_names or [])): and not getattr(args, "no_env_loras", False) and (env_names or [])):
env_lora_map = stage_env_loras(client, image_model, out_dir, env_names or [], env_lora_map = stage_env_loras(client, image_model, out_dir, env_names or [],
lora_steps=getattr(args, "env_lora_steps", 800), lora_steps=getattr(args, "env_lora_steps", 800),
lora_rank=getattr(args, "env_lora_rank", 16)) lora_rank=getattr(args, "env_lora_rank", 16),
train_base_model=getattr(args, "lora_train_base_model", None) or None)
# ── Stage 3: Videos ──────────────────────────────────────────────────────── # ── Stage 3: Videos ────────────────────────────────────────────────────────
if not args.skip_videos: if not args.skip_videos:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment