Case Study · 2026 Q2

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
01 / Architecture

系统架构

三层架构:本地桌面壳子 (pywebview frameless) ↔ 本地 FastAPI 后端 ↔ 远程 GPU pod。所有外部 I/O 经后端代理,前端不直接持有任何凭据。

L1 · FRONTEND · pywebview frameless desktop shell Custom Titlebar Mica · 12px round · drag SPA Console 5 surfaces · CSS tokens SSE Log Stream EventSource client JS↔Py Bridge window.pywebview.api L2 · BACKEND · FastAPI · localhost:8770 REST · SSE /api/* /api/events Pipeline Runner 7-stage orchestr. non-interactive Pod Trainer paramiko SSH env-driven script Param Engine vram × arch × tier → JSON preview LLM Agent tool-loop · 9 tools confirm-gate L3 · REMOTE GPU POD · SSH-driven training & inference Training Driver kohya / sd-scripts non-interactive shell Captioner tag / NL · multi-arch isolated venv ComfyUI Inference headless · localhost share ckpt + lora HF Mirror data + ckpt transit 5–15 MB/s up localhost HTTP / SSE js_api paramiko HF transit
实线 = 同进程 / 本机 HTTP 虚线 = 跨网络 (SSH · HF) 所有外部凭据仅驻留 L2 后端
02 / Interface

界面巡礼

统一 visionOS 风格暗色主题:near-black 底 + Apollo amber 强调色 + IBM Plex Mono。四个工作面板复用同一控制台骨架 (左表单 · 中画布 · 右副驾) ,sticky 顶栏切换。

Training pane
训练面板 · TRAIN
底模 · 主题类型 · 调参档位三段式表单;右侧画布展示训练日志 + 质量报告嵌入;底栏一键开训。
Queue pane
任务队列 · QUEUE
多任务串行队列;每条任务带状态徽章与重试入口;画布展示当前任务上下文 & 历史命中。
TTS pane
声音训练 · TTS
SBV2 pipeline 集成:底模选择 · 推理参数 · 数据集预览;与图像 LoRA 共享同一 pod 管线。
Sort pane
资源分拣 · SORT
数据集质量审查 · 多桶分包;接 YuNet 检测 + laplacian 评分;为训练前置卡点。
03 / Code Highlights

代码片段

从工程内挑出四段最能代表设计意图的代码:进程生命周期、实时通信、动态参数、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 没有任何自动执行危险动作的路径。
04 / Engineering Notes

工程亮点

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 综合分档实施。