Case Study · 2026 Q2
LoRA Studio · 端到端 LoRA 训练管线
LoRA Studio · 端到端 LoRA 训练管线
桌面应用
本地一站式 SDXL / Flux LoRA 训练编排程序。前端 pywebview frameless + Mica 一体化窗口, 后端 FastAPI 编排本地预处理 → 远程 GPU 训练 → 结果回灌的整条管线, 内嵌 LLM 助手作为操作副驾,全程 SSE 实时日志可视化。
Desktop App
Python · FastAPI
pywebview · Win32 · Mica
SSE Streaming
SSH / HuggingFace
LLM Tool-Loop
~3.2 k
Frontend LOC
7
Backend Modules
3-tier
Local · API · Pod
5
UI Surfaces
系统架构
三层架构:本地桌面壳子 (pywebview frameless) ↔ 本地 FastAPI 后端 ↔ 远程 GPU pod。所有外部 I/O 经后端代理,前端不直接持有任何凭据。
实线 = 同进程 / 本机 HTTP
虚线 = 跨网络 (SSH · HF)
所有外部凭据仅驻留 L2 后端
界面巡礼
统一 visionOS 风格暗色主题:near-black 底 + Apollo amber 强调色 + IBM Plex Mono。四个工作面板复用同一控制台骨架 (左表单 · 中画布 · 右副驾) ,sticky 顶栏切换。
训练面板 · TRAIN
底模 · 主题类型 · 调参档位三段式表单;右侧画布展示训练日志 + 质量报告嵌入;底栏一键开训。
任务队列 · QUEUE
多任务串行队列;每条任务带状态徽章与重试入口;画布展示当前任务上下文 & 历史命中。
声音训练 · TTS
SBV2 pipeline 集成:底模选择 · 推理参数 · 数据集预览;与图像 LoRA 共享同一 pod 管线。
资源分拣 · SORT
数据集质量审查 · 多桶分包;接 YuNet 检测 + laplacian 评分;为训练前置卡点。
代码片段
从工程内挑出四段最能代表设计意图的代码:进程生命周期、实时通信、动态参数、LLM 安全 gate。
launcher.py
Win32 Job Object · 关窗自动清子进程
# launcher 被强结时把所有子进程一起带走 (KILL_ON_JOB_CLOSE) def _job_bind_children() -> None: global _job_handle JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000 JobObjectExtendedLimitInformation = 9 h = ctypes.windll.kernel32.CreateJobObjectW(None, None) if not h: return # 设置 ExtendedLimitInfo, 让 launcher 进程退出时 OS 清理子进程 info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION() info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE ctypes.windll.kernel32.SetInformationJobObject( h, JobObjectExtendedLimitInformation, ctypes.byref(info), ctypes.sizeof(info)) # 把当前进程绑进 job, 后续 spawn 的子进程自动继承 ctypes.windll.kernel32.AssignProcessToJobObject( h, ctypes.windll.kernel32.GetCurrentProcess()) _job_handle = h
关注点: 桌面应用的常见痛点是异常退出后僵尸 backend。Job Object 把进程生命周期托管给 OS,无需 PID 文件、无需信号处理,强结 / 任务管理器 KILL 也跑得通。
backend/server.py
SSE 流式日志 · 不堵事件循环
# ----- SSE 实时日志 ----- @app.get("/api/events") async def api_events(request: Request): async def gen(): last = 0 yield "retry: 2000\n\n" # 客户端断线 2s 重连 while True: if await request.is_disconnected(): break new, nxt, phase, stages, error = STATE.logs_since(last) last = nxt payload = { "logs": new, "phase": phase, "stages": stages, "error": error, "has_report": bool(STATE.quality_report_path), "result": dict(STATE.result), } yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n" await asyncio.sleep(0.4) return StreamingResponse(gen(), media_type="text/event-stream")
关注点: 训练日志可能数万行,HTTP 长轮询会撑爆。SSE +
logs_since(cursor) 只回推增量,is_disconnected() 防止断线后协程泄漏。前端 EventSource 直连,零 polyfill。
backend/param_engine.py
动态训练参数 · 显存 × 架构 × 档位
def compute_train_params(img_count, gpu_vram_gb, arch_family, subject_type, quality_tier, overrides=None, single_image=False, photoreal=False): img_count = max(1, int(img_count)) vram = int(gpu_vram_gb or 0) is_style = (subject_type == "style") # 分辨率 × batch 阶梯, 防 OOM (SDXL 24G 报 23 → 阈值 22) if arch_family == "flux": reso, batch = 1024, 1 elif arch_family == "sdxl": reso, batch = 640, 1 for mn, r, b in [(48,1024,6), (40,1024,4), (30,1024,3), (22,1024,2), (14,768,1), (0,640,1)]: if vram >= mn: reso, batch = r, b break # 步数 · dim · 学习率: 风格 vs 主体 / 通用 vs photoreal 分档 spi = {"fast": 10, "balanced": 20, "hq": 35}[quality_tier] lo, hi = {"hq": (1500, 3500)}.get(quality_tier, (500, 2500)) steps = max(lo, min(hi, spi * img_count)) dim = 32 if quality_tier == "hq" else 16 unet_lr = "1e-4" if photoreal else "3e-4" # 防写实过拟合 return {"resolution": reso, "batch": batch, "steps": steps, "network_dim": dim, "unet_lr": unet_lr, ...}
关注点: 训练参数对硬件 / 主题 / 档位都敏感,写死 = 用户改不了;全开 = 用户不会调。引擎只输出"推荐值 + 显存校验",提供
overrides 出口给高级面板覆盖,UI 实时预览。
backend/agent.py
LLM Tool-Loop · 危险动作必走 confirm-gate
# 9 个工具中, 烧钱/动 pod/删除 的 4 个 action 不进白名单 # LLM 永远只能 "request_confirm", 由前端弹原生确认框, 用户亲点才执行 def _exec_tool(name, args, ctx): if name == "get_pipeline_state": # 读 · 安全 return json.dumps(STATE.snapshot(), ensure_ascii=False) if name == "preview_train_params": # 读 · 安全 params = param_engine.compute_train_params(...) return json.dumps({"params": params}, ensure_ascii=False) if name == "set_form_fields": # 改 UI · 字段白名单 applied, rejected = {}, [] for u in args.get("updates") or []: field = str((u or {}).get("field", "")) if field not in SETTABLE_FIELDS: rejected.append(field); continue ctx["form_actions"].append({"field": field, "value": ...}) return json.dumps({"applied": applied, "rejected": rejected}) if name == "request_confirm": # 危险动作 · 不执行只登记 action = str(args.get("action", "")) if action not in ("start", "test_pod", "confirm_train", "abort"): return json.dumps({"ok": False, "error": "unknown action"}) ctx["confirm"] = {"action": action, "summary": args.get("summary")} return json.dumps({"ok": True, "note": "确认请求已登记并弹给用户。你不能自己执行。"})
关注点: agent 时代的安全分层。读操作开放 (状态 · 参数预览 · 表单读);写 UI 字段白名单过滤;任何动 GPU / 动钱 / 删数据的 action 全转
request_confirm,由人最终拍板。LLM 没有任何自动执行危险动作的路径。工程亮点
FRAMELESS WINDOW
pywebview frameless + transparent=False + DWM Mica/圆角/暗色标题,自绘 topbar 实现 Win32 ↔ web 一致体验。drag/min/max/close 全 JS↔Py 桥接。
VISION-OS THEME
近黑 + Apollo amber + IBM Plex Mono 统一色系。撤所有装饰层 (film-grain / sheen / CRT),token 化字号/间距,WCAG 4.5:1 对比度合规。
NON-INTERACTIVE PIPELINE
复用现成 8 个数据预处理脚本;后端用 ENV + flag 屏蔽所有交互菜单,全部经 stdlib subprocess 直跑,零 GUI 弹窗。
ENV-DRIVEN TRAINING
远程训练脚本统一 env 参数化;后端动态参数引擎计算 → 经 SSH heredoc 部署 → 流回训练日志。本地 < 5 行配置即可换底模 / 换档位。
SHARED MODEL POOL
同一 GPU pod 跑训练 + 推理。
extra_model_paths.yaml 让 ComfyUI 自动识别 /workspace/output 下的新 LoRA,零额外配置。ADVERSARIAL UI REVIEW
前端品质用 4-agent fan-out 对抗审查 (截图证据 / 用户视角 / 设计师视角 / 工程师视角),输出 100+ 条 critique,主 agent 综合分档实施。