脸部LoRA训练项目:素材流水线 + Gradio控制台 + RunPod云端续炼方案(v7续炼完成)

This commit is contained in:
hmo
2026-08-10 10:10:34 +08:00
commit 77d0cbd0cb
29 changed files with 5898 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
output/*
!output/run_cache.py
training/
cloud/dataset.zip
tools/perf_test/
tools/test/
*.log
__pycache__/
*.pyc
tools/gui_state.json
tools/gui_dir.txt
# 敏感文件(禁止提交)
.runpod_api_key
env.local
tools/gui_label_backup.json
**/gui_label_backup.json
# 大文件/训练产物
temp/
*.safetensors
cloud/*.zip
+181
View File
@@ -0,0 +1,181 @@
# 脸部 LoRA 训练项目(Qwen-Rapid-AIO 匹配版)
> 创建:2026-08-03
> 目标:用老莫的照片训练一个脸部 LoRA,让 AI 出图永久自带他的脸,配合常用 checkpoint `Qwen-Rapid-AIO-NSFW-v23` 使用
## 项目背景
- 灵感来源:[Z:\wiki\raw\articles\2026-08-02-z-image-turbo-face-lora-训练实操.md]Z-Image-Turbo 路线,已验证技术可信但模型不匹配)
- 老莫实际模型:`Qwen-Rapid-AIO-NSFW-v23.safetensors`26.48GB,位于 `D:\AI\sd\novelai-webui-aki-v3-r\models\Stable-diffusion\`
- 该 checkpoint 本质:HuggingFace `Phr00t/Qwen-Image-Edit-Rapid-AIO` 融合模型 = **Qwen-Image-Edit-2511 底模(20B MMDiT** + Lightning 4/8 步蒸馏 LoRA + NSFW LoRAs + VAE + Qwen2.5-VL CLIPFP8
- 结论:LoRA 必须在 **Edit-2511 bf16 官方底模**上训练(不能直接用 FP8 融合模型),训完叠加回 Rapid-AIO 推理
## 硬件环境
| 项 | 值 |
|---|---|
| GPU | RTX 3080 Laptop GPU **16GB**Ampere, sm_86 |
| 驱动 | 591.74 / CUDA 13.1 |
| 内存 | **32GB**(无 64G,靠 fp8 量化 + TE 预缓存压内存) |
| 存储 | D 盘 153GB 空闲 |
## 训练方案(定稿)
**工具**kohya musubi-tuner`D:\AI\sd\musubi-tuner`v0.3.4
**底模**Qwen-Image-Edit-2511 bf16(从 ModelScope 下载 diffusers 分片,musubi `load_split_weights` 原生支持分片,无需转换)
### 核心参数
```
--model_version edit-2511
--network_module networks.lora_qwen_image --network_dim 16
--learning_rate 5e-5 --optimizer_type adamw8bit
--fp8_base --fp8_scaled --fp8_vl --gradient_checkpointing --blocks_to_swap 32
--max_train_epochs 80(≈1600步)
--timestep_sampling shift --discrete_flow_shift 2.2
分辨率 1024×1024
```
### 内存策略(32G 关键)
1. `--fp8_base --fp8_scaled`DiT 权重 fp8 存储(40GB→20GB
2. 预缓存 text encoder 输出(`qwen_image_cache_text_encoder_outputs.py`):训练时不加载 TE(省 7GB
3. pagefile 设 64GSSD)当兜底保险
4. 训练时关闭浏览器等大内存应用
### 实测速度(2026-08-05 冒烟实测)
| 系统状态 | 速度 | 1600 步预估 |
|---|---|---|
| ComfyUI 运行中(空闲占 4.6GB VRAM | **503-670 s/步**(页面抖动,逐步变慢) | **~12 天,不可行** |
| 干净系统(初次冒烟) | ~55 s/步 | ~24 小时,隔夜可跑 |
**启动条件(必须)**:关闭 ComfyUI + 浏览器等大内存程序 → 双击 `启动训练.bat`bat 有前置检查:ComfyUI 在跑则拒绝启动、commit <30GB 拒绝启动)。
**根因**32GB 物理内存跑 20B fp8 是极限状态,blocks_to_swap 24 的 CPU↔GPU 交换在内存不足时落到 pagefile(SSD),速度跌 10 倍+。训练必须独占物理内存。
**共存验证**ComfyUI 空闲时 VRAM 11.8GB 下训练可跑通(不 OOM),但速度不可接受,仅作应急参考。
## 文件位置
| 用途 | 路径 |
|---|---|
| 训练素材(照片) | `素材/` |
| 打标 caption | `打标/` |
| 训练配置 | `config/` |
| LoRA 输出 | `output/` |
| 参考资料 | `资料/` |
| musubi-tuner | `D:\AI\sd\musubi-tuner` |
| 模型文件 | `D:\AI\sd\models\qwen-edit-2511\`transformer 5 分片 + text_encoder 4 分片 + vae |
| VAE(已有) | `D:\AI\sd\ComfyUI\ComfyUI_windows_portable_2\ComfyUI\models\vae\qwen_image_vae.safetensors` |
## 流程
1. [x] 调研文章 + Qwen 路线验证(2026-08-03
2. [x] 排查本地资源(VAE 已有,DiT/TE 需下载)
3. [x] 立项 + musubi-tuner 就位
4. [x] 下载模型完成(DiT 38GB + TE 15GB + VAEModelScope 8MB/s
5. [x] uv 环境装依赖(cu132torch 2.13 + CUDA 验证通过)
6. [x] 开发 face_checker 辅助工具(审核 + 智能选图,测试通过)
7. [ ] 冒烟训练验证(分片加载 + fp8 + swap 内存,3 步小样)
8. [ ] 素材准备(老莫选照片 → face_checker pick → batch 整理)
9. [ ] 打标(触发词 + 场景描述,不描述五官)
10. [ ] 正式训练(1024 + blocks_to_swap 32 + 隔夜)
11. [ ] checkpoint 对比(每 10 epoch 存档挑最佳)
12. [ ] 叠加 Rapid-AIO 推理验证
## 辅助工具(全部就绪)
| 工具 | 位置 | 功能 |
|---|---|---|
| **auto 流水线** | `tools/face_checker.py auto` | **丢一堆照片 → 自动分类裁剪打标**(见下) |
| face_checker | `tools/face_checker.py` | 审核/选图/整理/合影裁切(check/pick/batch/crop |
| **打标 GUI** | `tools/caption_gui.py` | Gradio 网页(localhost:7860),**支持目录切换** |
| **融合工具** | `tools/merge_fixed.py` | **原图 + ComfyUI 修改图 无缝合并**(见下) |
| 验证脚本 | `tools/validate_lora.py` | 训练后出图对比(备选) |
### merge_fixed 融合工具(ComfyUI inpainting 后处理)
**问题**:ComfyUI 移除合影人物时,即使指定"非修改区域不得漂移",全图清晰度仍损失。
**方案**:**按修改幅度连续分级融合** —— 有限修改区保留原图像素,明显修改区用修改图像素,中间平滑过渡。
```
python tools\merge_fixed.py <原图目录> <修改图目录> <输出目录> [--alpha-threshold 15] [--softness 6]
```
**核心规则**(老莫定):
| 区域 | 修改幅度 | 用谁的像素 |
|---|---|---|
| 有限修改(放大才看出,如全图降质/面部) | diff 小 | **原图精确像素**alpha≈0 |
| 明显修改(一眼看出,如人物移除) | diff 大 | **修改图像素**alpha≈1 |
| 中间 | 平滑过渡 | sigmoid 渐变(无接缝) |
- 算法:Lab 色彩空间差异 → 高斯模糊去结构噪声 → **sigmoid 连续 alpha** → 加权混合
- **面部保护**(默认开,`--no-protect-face` 关):检测修改图残留人脸,强制原图像素兜底
- **验证**:面部 MAE **0.000**(逐像素零改变)、被移除区干净用修改图、过渡无接缝
**参数**
| 参数 | 默认 | 说明 |
|---|---|---|
| `--alpha-threshold` | 15 | 修改幅度分界。残留残影时调低到 8-10(更多区域归修改图) |
| `--softness` | 6 | 过渡带宽度,越大越平缓 |
| `--no-protect-face` | - | 关闭面部强制保护 |
**注意**:残影分两种——① 人物边缘未清除的痕迹(调低 alpha-threshold);② inpainting 区域内部 AI 生成的残留(ComfyUI 质量问题,融合救不了)。
### auto 全自动流水线(v2,核心!)
```
python tools\face_checker.py auto <照片目录> --out <输出目录> [--limit 20]
```
自动完成:
1. **质量评分(0-100**:人脸绝对像素(40) + 面部清晰度(35) + 分辨率(25) —— **脸小但锐利 > 脸大但糊**
2. **清晰度区分**:面部 Laplacian vs 整体 Laplacian(脸糊=淘汰;背景糊但脸清楚=可用)
3. **构图分类+自动裁剪**(占比 + 脸像素双指标):
- 脸占比 ≥8% **或** 脸短边 ≥256px → 裁**面部特写**(人脸居中放大 1.8 倍)
- 脸占比 ≥2% 且脸 ≥150px → 保留**半身**
- 检测到完整人体 → 裁**全身构图**
- 多人合影 → 自动裁最大人脸;裁后短边不足 → 降级保留原图
4. **超量筛选**(--limit 20):每类内部 质量优先 + pHash 多样性贪心,自动挑最优(默认 特写10/半身6/全身4)
5. **淘汰**:无人脸/脸糊/脸太小无人体/分辨率不足 → 移入 淘汰/ + 原因
6. **打标草稿**:按类型自动生成 caption txtGUI 里微调
输出:`out/特写/``out/半身/``out/全身/``out/未选中/``out/淘汰/`
**验证证据**:脸43px极清晰(1260) → 保留;脸大但模糊 → 淘汰。34 张输入 → 自动筛选 12 张最优。
### 老莫使用流程(最终版)
1. 照片全丢进 `photos_原始/`(不挑,合影也无所谓)
2.`auto photos_原始 --out photos_auto` → 自动分类裁剪打标
3. 打开 http://127.0.0.1:7860 → 顶部输入 `photos_auto` → 加载 → 微调 caption/发型 → 保存
4. 一键整理训练集 → 喊我开训
## 训练自动样本图(已验证)
- `config/sample_prompts.txt`:4 个验证场景(证件照/日常/户外全身/半侧脸)
- 训练脚本已加 `--sample_prompts --sample_every_n_epochs 10`
- 每 10 epoch 自动出 4 张样本图到 `output/checkpoints/sample/`,隔天看样本图即可判断 LoRA 演化
- 冒烟验证通过(EXIT 0,样本图正常生成)
## 使用流程(最终版)
1. 老莫选 15-25 张照片 → 丢进 `photos/`
2. 浏览器开 `http://127.0.0.1:7860` → 审核/选类型/填发型/改 caption → 保存 → 一键整理
3.`config\训练脚本.py`80 epoch ≈ 1600 步 + 自动样本图,约 25 小时隔夜跑)
4.`output/checkpoints/sample/` 样本图挑最佳 checkpoint
5. checkpoint 复制到 WebUI `models\Lora\qwen\`,加载 Rapid-AIO + LoRA 出图验证
## 训练验证要点
- musubi 分片加载:`--dit` 指向 `...-00001-of-00005.safetensors` 自动合并(源码确认)
- Edit-2511 无 control image 时**自动降级 T2I 训练**(源码 117-118 行确认)→ 脸部 LoRA 无需编辑图
- 数据集 toml **不能有 BOM**(toml 库报错)→ 用无 BOM UTF-8 写
- 模型路径保持 ASCII`D:\AI\sd\models\...`),数据集目录中文路径 PIL 可读
## 伦理与合规
- 只训老莫自己的脸,不涉及他人照片
- 生成内容公开使用按规标注"AI 生成"
## 关键结论备忘
- musubi 支持分片加载:`--dit` 指向 `diffusion_pytorch_model-00001-of-00005.safetensors` 自动合并全部 5 分片(`load_split_weights` 源码确认)
- fp8_scaled / fp8_e4m3fn 版本**不能**用于训练(musubi 文档明确)
- 训练底模用 bf16 官方权重;推理用 Rapid-AIOFP8)叠加 LoRA
+77
View File
@@ -0,0 +1,77 @@
#!/bin/bash
# ============================================================
# RunPod Secure Cloud 一键引导:Qwen-Image-Edit-2511 脸部 LoRA 训练
# 用法:pod 启动后,在 web terminal 里执行:
# cd /workspace && unzip dataset.zip -d train_dataset && bash bootstrap.sh
# ============================================================
set -e
echo "===== [1/4] 安装 musubi-tuner ====="
cd /workspace
if [ ! -d musubi-tuner ]; then
git clone --depth 1 https://github.com/kohya-ss/musubi-tuner.git
fi
cd musubi-tuner
pip install -e . --quiet
pip install --quiet huggingface_hub hf_transfer
export HF_HUB_ENABLE_HF_TRANSFER=1
echo "===== [2/4] 下载模型(Qwen-Image-Edit-2511,机房网速约2-5分钟)====="
mkdir -p /workspace/models/qwen-edit-2511/{transformer,text_encoder}
hf download Qwen/Qwen-Image-Edit-2511 \
--include "transformer/*" \
--local-dir /workspace/models/qwen-edit-2511
hf download Qwen/Qwen-Image-Edit-2511 \
--include "text_encoder/*" \
--local-dir /workspace/models/qwen-edit-2511
hf download Qwen/Qwen-Image-Edit-2511 \
--include "vae/diffusion_pytorch_model.safetensors" \
--local-dir /workspace/models/qwen-edit-2511
# VAE 路径兼容(musubi 参数直接指到文件)
find /workspace/models/qwen-edit-2511 -name "*.safetensors" | head -20
echo "===== [3/4] 数据集配置 ====="
mkdir -p /workspace/train_dataset /workspace/config /workspace/ckpt
cat > /workspace/config/dataset.toml <<'EOF'
[general]
resolution = 1024
caption_extension = ".txt"
batch_size = 1
enable_bucket = true
bucket_no_upscale = false
[[datasets]]
image_directory = "/workspace/train_dataset"
num_repeats = 1
EOF
cp /workspace/sample_prompts.txt /workspace/config/ 2>/dev/null || true
echo "===== [3.5] 续炼 checkpoint 就位 ====="
# 续炼起点:本地 scp 上传的 myface_lora-000060.safetensors 在 /workspace/
# 1.1GBdim32 版本),移到 /workspace/ckpt/ 供 --network_weights 使用
if [ -f /workspace/myface_lora-000060.safetensors ]; then
cp /workspace/myface_lora-000060.safetensors /workspace/ckpt/
echo "checkpoint 就位: $(ls -la /workspace/ckpt/myface_lora-000060.safetensors)"
else
echo "WARNING: /workspace/myface_lora-000060.safetensors 不存在!续炼会失败(除非从头训练)"
fi
echo "===== [4/4] 预缓存(VAE latent + TextEncoder====="
MODELS=/workspace/models/qwen-edit-2511
DIT=$MODELS/transformer/$(ls $MODELS/transformer | grep '00001-of' | head -1)
TE=$MODELS/text_encoder/$(ls $MODELS/text_encoder | grep '00001-of' | head -1)
VAE=$(find $MODELS/vae -name "*.safetensors" | head -1)
echo "DIT: $DIT"
echo "TE: $TE"
echo "VAE: $VAE"
python src/musubi_tuner/qwen_image_cache_latents.py \
--dataset_config /workspace/config/dataset.toml \
--vae "$VAE" --device cuda --model_version edit-2511
python src/musubi_tuner/qwen_image_cache_text_encoder_outputs.py \
--dataset_config /workspace/config/dataset.toml \
--text_encoder "$TE" --fp8_vl --device cuda --model_version edit-2511
echo ""
echo "===== 全部就绪!启动训练执行:bash train.sh ====="
+1
View File
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCmWjWSUaZFn3DD0MX3W7+BkTDoJvC+ssWYlxBFrMSB3MOm3HpBMySB1BBBy6J3tEcEPP+zngZFuSTBWvSOaUMXCcvkd4RKFkGzhRjLIRN0yrMI4EH2KoObv7hTSvma4R0DUQoFp/GuFu8dNbZOfcfGKFtBAfvf2mynkod/RDNd89K9pqpJxzWgBdYAuHgVelXiF+hnG34zH+w6EXQmk0+91YDdHQX9F8yszE561ATNOxvPpAIETuo9tNNaOlS7FnZUR0vva6AzMY7/hlkxVIZGwRwayzdRUG8VGh0nLkwc7o9MFW4HiUq+WtQTgePPTJ3RuNBNVi8NdeLfr6uT5dh5xmq6QAWuN7OjZkb4fPYe7RETDSukMdaqv9YtjwYKTRuva+JuAEJdgZr6Ar8jXAh7k63hKQWNCtIp3QEnbn79HOCsRpweFcc77Z3JmFLRU4JLBp3iqmtZAniz465JOsjhIHdfHVWS1hP7S2oIe/GDwL6ts30clOTNctJlF43xWa0FoRN83JZgKaXghiBoT1AX9gK0Y5uZYCeN9/qJkdq4TJtFWO16pGFKmvE2OyIGr9q+tLEVUsxqjoFTlaUoqHBRTAq+danZfv7wLTnjX8ZTTXHvON2ethJInt6vRhdxz2fgH77pZP/3mZLbd9h8YGQCxyxkLO898nn32wu2SUXDYw== hmo@daily-workspace
+5
View File
@@ -0,0 +1,5 @@
# 训练时自动出样本图(验证 LoRA 演化,每行一个 prompt,# 开头是注释)
lm_face_v1, professional headshot, business attire, neutral expression, studio lighting, plain white background
lm_face_v1, natural lifestyle portrait, by a window with soft diffused sunlight, warm tones
lm_face_v1, full body shot, standing in a park, natural daylight, casual clothing
lm_face_v1, three-quarter view portrait, soft golden hour light, shallow depth of field
+44
View File
@@ -0,0 +1,44 @@
#!/bin/bash
# ============================================================
# 正式训练(RunPod Secure Cloud 4090 24G
# 用法:nohup bash train.sh > /workspace/train.log 2>&1 &
# 特性:崩溃自动重启(进程退出后自动拉起续跑,直到完成或 epoch120 checkpoint 出现)
# ============================================================
cd /workspace/musubi-tuner
MODELS=/workspace/models/qwen-edit-2511
DIT=$MODELS/transformer/$(ls $MODELS/transformer | grep '00001-of' | head -1)
TE=$MODELS/text_encoder/$(ls $MODELS/text_encoder | grep '00001-of' | head -1)
VAE=$(find $MODELS/vae -name "*.safetensors" | head -1)
TRAIN_CMD="accelerate launch --num_cpu_threads_per_process 2 --mixed_precision bf16 \
src/musubi_tuner/qwen_image_train_network.py \
--dit \"$DIT\" --vae \"$VAE\" --text_encoder \"$TE\" \
--model_version edit-2511 \
--dataset_config /workspace/config/dataset.toml \
--sdpa --mixed_precision bf16 \
--timestep_sampling shift --weighting_scheme none --discrete_flow_shift 2.2 \
--optimizer_type adamw8bit --learning_rate 1e-4 \
--gradient_checkpointing \
--network_module networks.lora_qwen_image --network_dim 32 \\
--network_weights /workspace/ckpt/myface_lora-000060.safetensors \
--fp8_base --fp8_scaled --fp8_vl --blocks_to_swap 8 \
--max_train_epochs 50 --save_every_n_epochs 10 --seed 42 \
--sample_prompts /workspace/config/sample_prompts.txt --sample_every_n_epochs 10 \
--output_dir /workspace/ckpt --output_name myface_v7"
# 崩溃自动重启:最多 20 次(防止死循环烧钱)
for attempt in $(seq 1 20); do
echo "===== [尝试 $attempt/20] 启动训练 $(date) ====="
eval "$TRAIN_CMD"
rc=$?
echo "===== 训练退出 rc=$rc $(date) ====="
# 完成检测:epoch120 checkpoint 已存在 → 训练成功结束
if ls /workspace/ckpt/myface_v7-000050.safetensors >/dev/null 2>&1; then
echo "===== epoch120 checkpoint 已生成,训练完成 ====="
break
fi
# 中途崩溃 → 等待 30s 后自动重启续跑
echo "===== 训练中断(rc=$rc),30 秒后自动重启续跑 ====="
sleep 30
done
echo "===== train.sh 结束 $(date) ====="
+13
View File
@@ -0,0 +1,13 @@
# musubi-tuner 数据集配置(脸部 LoRA / Edit-2511 T2I
# 无 control_directory → musubi 自动按 text-to-image 训练
[general]
resolution = 1024
caption_extension = ".txt"
batch_size = 1
enable_bucket = true
bucket_no_upscale = false
[[datasets]]
image_directory = "D:/F/NewI/opencode/daily-workspace/projects/脸部LoRA训练-Qwen-Image/output/train_dataset"
num_repeats = 1
+11
View File
@@ -0,0 +1,11 @@
# 由 ⑤ 训练 Tab 重新预缓存生成
[general]
resolution = 1024
caption_extension = ".txt"
batch_size = 1
enable_bucket = true
bucket_no_upscale = false
[[datasets]]
image_directory = "K:/AI/training/ldf/singled/5. autohandling_v7/train_dataset"
num_repeats = 1
+11
View File
@@ -0,0 +1,11 @@
# musubi-tuner 冒烟测试数据集配置
[general]
resolution = 1024
caption_extension = ".txt"
batch_size = 1
enable_bucket = true
bucket_no_upscale = false
[[datasets]]
image_directory = "D:/F/NewI/opencode/daily-workspace/projects/脸部LoRA训练-Qwen-Image/output/smoke_dataset"
num_repeats = 1
+5
View File
@@ -0,0 +1,5 @@
# 训练时自动出样本图(验证 LoRA 演化,每行一个 prompt,# 开头是注释)
lm_face_v1, professional headshot, business attire, neutral expression, studio lighting, plain white background
lm_face_v1, natural lifestyle portrait, by a window with soft diffused sunlight, warm tones
lm_face_v1, full body shot, standing in a park, natural daylight, casual clothing
lm_face_v1, three-quarter view portrait, soft golden hour light, shallow depth of field
+4
View File
@@ -0,0 +1,4 @@
{
"train_dataset": "K:\\AI\\training\\ldf\\singled\\5. autohandling_v7\\train_dataset",
"output_dir": "K:\\AI\\training\\ldf\\singled\\5. autohandling_v7\\output\\checkpoints"
}
+29
View File
@@ -0,0 +1,29 @@
# 脸部 LoRA 训练脚本(Qwen-Image Edit-2511 / 16G 显存 / 32G 内存优化版)
# 用法:powershell -ExecutionPolicy Bypass -File 训练脚本.ps1
# 前置:素材已整理到 output/train_dataset/img_001.jpg + img_001.txt 打标)
$ErrorActionPreference = "Stop"
$env:PYTHONUTF8 = "1"
$VENV = "D:\AI\sd\musubi-tuner\.venv\Scripts"
$TOOL = "D:\AI\sd\musubi-tuner\src\musubi_tuner"
$MODELS = "D:\AI\sd\models\qwen-edit-2511"
$PROJ = "D:\F\NewI\opencode\daily-workspace\projects\脸部LoRA训练-Qwen-Image"
$env:HF_HUB_OFFLINE = "1" # 全程离线,避免联网
& "$VENV\accelerate.exe" launch --num_cpu_threads_per_process 1 --mixed_precision bf16 `
"$TOOL\qwen_image_train_network.py" `
--dit "$MODELS\transformer\diffusion_pytorch_model-00001-of-00005.safetensors" `
--vae "$MODELS\diffusion_pytorch_model.safetensors" `
--text_encoder "$MODELS\text_encoder\model-00001-of-00004.safetensors" `
--model_version edit-2511 `
--dataset_config "$PROJ\config\dataset.toml" `
--sdpa --mixed_precision bf16 `
--timestep_sampling shift --weighting_scheme none --discrete_flow_shift 2.2 `
--optimizer_type adamw8bit --learning_rate 5e-5 `
--gradient_checkpointing `
--network_module networks.lora_qwen_image --network_dim 16 `
--fp8_base --fp8_scaled --fp8_vl --blocks_to_swap 32 `
--max_train_epochs 80 --save_every_n_epochs 10 --seed 42 `
--output_dir "$PROJ\output\checkpoints" --output_name myface_lora
+76
View File
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
"""
正式训练脚本:脸部 LoRAQwen-Image Edit-2511 / 16G 显存 / 32G 内存优化)
用法: python 训练脚本.py
前置: output/train_dataset/ 里有 img_001.jpg + img_001.txt 打标
"""
import json
import os
import subprocess
import sys
os.environ["PYTHONUTF8"] = "1"
os.environ["HF_HUB_OFFLINE"] = "1"
VENV = r"M:\AI\sd\musubi-tuner\.venv\Scripts"
PROJ = r"D:\F\NewI\opencode\daily-workspace\projects\脸部LoRA训练-Qwen-Image"
MODELS = r"M:\AI\sd\models\qwen-edit-2511"
# 动态目录配置(GUI 训练 Tab 写入 config/train_config.json
_cfg = {}
_cfg_path = os.path.join(PROJ, "config", "train_config.json")
if os.path.exists(_cfg_path):
try:
_cfg = json.load(open(_cfg_path, encoding="utf-8-sig")) # utf-8-sig 处理 BOM
except Exception as _e:
print(f"[WARN] train_config.json 读取失败: {_e}")
_cfg = {}
TRAIN_DATASET = _cfg.get("train_dataset", os.path.join(PROJ, "output", "train_dataset"))
CKPT_OUT = _cfg.get("output_dir", os.path.join(PROJ, "output", "checkpoints"))
# 动态生成数据集配置(image_directory 指向所选训练集)
_dataset_toml = os.path.join(PROJ, "config", "dataset_active.toml")
_toml_body = f"""# 由 GUI 训练 Tab 动态生成(勿手改)
[general]
resolution = 1024
caption_extension = ".txt"
batch_size = 1
enable_bucket = true
bucket_no_upscale = false
[[datasets]]
image_directory = "{TRAIN_DATASET.replace(chr(92), '/')}"
num_repeats = 1
"""
with open(_dataset_toml, "w", encoding="utf-8") as _f:
_f.write(_toml_body)
cmd = [
os.path.join(VENV, "accelerate.exe"), "launch",
"--num_cpu_threads_per_process", "1", "--mixed_precision", "bf16",
r"D:\AI\sd\musubi-tuner\src\musubi_tuner\qwen_image_train_network.py",
"--dit", os.path.join(MODELS, "transformer", "diffusion_pytorch_model-00001-of-00005.safetensors"),
"--vae", os.path.join(MODELS, "diffusion_pytorch_model.safetensors"),
"--text_encoder", os.path.join(MODELS, "text_encoder", "model-00001-of-00004.safetensors"),
"--model_version", "edit-2511",
"--dataset_config", _dataset_toml,
"--sdpa", "--mixed_precision", "bf16",
"--timestep_sampling", "shift", "--weighting_scheme", "none", "--discrete_flow_shift", "2.2",
"--optimizer_type", "adamw8bit", "--learning_rate", "5e-5",
"--gradient_checkpointing",
"--network_module", "networks.lora_qwen_image", "--network_dim", "16",
# 续炼:加载旧 LoRA 000060(脸已成形、细节未过度固化),用重调后的 v7 数据增量修正标签
"--network_weights", r"M:\AI\sd\novelai-webui-aki-v3-r\models\Lora\v6\myface_lora-000060.safetensors",
"--fp8_base", "--fp8_scaled", "--fp8_vl", "--blocks_to_swap", "24",
"--max_train_epochs", "50", "--save_every_n_epochs", "10", "--seed", "42",
"--sample_prompts", os.path.join(PROJ, "config", "sample_prompts.txt"),
"--sample_every_n_epochs", "10",
"--output_dir", CKPT_OUT,
"--output_name", "myface_v7",
]
print("训练集:", TRAIN_DATASET)
print("输出目录:", CKPT_OUT)
print("CMD:", " ".join(cmd))
r = subprocess.run(cmd)
print("EXIT CODE:", r.returncode)
sys.exit(r.returncode)
+312
View File
@@ -0,0 +1,312 @@
# 脸部 LoRA 云端训练 · 完整操作文档(v2 复盘 + v3 方案)
> 建立:2026-08-09
> 性质:**训练全流程唯一权威参考**(含上次全部踩坑记录)
> 铁律:**训练期间不要手动干预;监控全自动;用户睡觉期间 AI 不执行任何未经文档确认的操作**
## 本次训练实况(v3 最终版,2026-08-09
| 项 | 值 |
|---|---|
| pod id | `k1nzrqh8lfj7jg`**RTX 4090** 24GB$0.74/h |
| IP/SSH | 103.196.86.68 / 50155 |
| 镜像 | runpod/pytorch:2.4.0-py3.11-cuda12.4.1-devel-ubuntu22.04 |
| blocks_to_swap | **8**4090 24GB,与 v2 一致) |
| 训练 | dim32/lr1e-4/120ep/32张=3840步,实测 **2.5s/步**ETA ~2.5h |
| 预缓存 | ✅ 已验证 128 文件(32 img + 32 txt + 32 VAE `_qie` + 32 TE `_qie_te` |
| 监控 | monitor_v3 + 计划任务 FaceLoRA-Monitor-v3(每5分钟) |
| 下载 | `https://k1nzrqh8lfj7jg-8888.proxy.runpod.net/`HTTP 200 已验证) |
| 预算 | max_hours 4$0.74/h × 4 = **$2.96 封顶** |
> ⚠️ **GPU 选择结论(踩坑记录,2026-08-09**
> - ❌ **RTX 5090Blackwell sm_120)不可用**——镜像 PyTorch 2.4 只支持到 sm_90VAE 预缓存 GPU 编码直接失败(`sm_120 not compatible`),浪费 55GB 下载 + 烧钱
> - ❌ **A40 可用但慢**——实测 5.33s/步(算力 ~37 TFLOPS,是 4090 一半)
> - ✅ **RTX 4090 最优**——实测 **2.5s/步**,且是 v2 完整验证过的(上次跑完 2400 步)
> - **教训**:换 GPU 前必须确认「镜像 PyTorch 的 CUDA capability ≤ GPU 架构」!5090=sm_120、4090/A40=sm_86/89PyTorch 2.4 支持 sm_50-90
> ⚠️ **预缓存验证方法(踩坑记录)**:Qwen 的缓存文件是 **`.safetensors``img_xxx_..._qie.safetensors` + `_qie_te.safetensors`),不是 `.npz`**!判断缓存成功要看 `_qie.safetensors` 数量 = 素材数,别查 .npz(会误判为 0 导致误删/重复折腾)。
---
## 一、项目目标与当前状态
**目标**:用老莫(用户)老婆的照片训练脸部 LoRA,叠加 `Qwen-Rapid-AIO-NSFW-v23` checkpoint 出图。
**训练底模**Qwen-Image-Edit-2511 bf16 官方底模(不能用 FP8 融合模型训练)。
**当前素材状态**2026-08-09):
- 已选 32 张(特写 21 / 半身 8 / 全身 3),无冗余(已删重复)
- 大笑 3 张(稀缺)、表情 6 种、光照多样
- 训练集已生成:`K:\AI\training\ldf\singled\5. autohandling_v6\train_dataset\`img_001..032 + txt
- dataset.zip 已打包:`cloud/dataset.zip`20.8MB32 img + 32 txt = 64 文件)
- 余额:**$10 已充值**(预计本次花费 $3-4,足够)
---
## 二、训练参数(最终定稿)
| 参数 | 值 | 说明 |
|---|---|---|
| network_dim | 32 | v1(dim16) 验证"不像"→ 调大;v2 未验证效果 |
| learning_rate | 1e-4 | 与 dim32 配套 |
| max_train_epochs | 120 | 32 张 × 120 = **3840 步** |
| blocks_to_swap | 8 | 4090 24GB + 62GB RAM 够用 |
| fp8_base/scaled/vl | on | 省显存 |
| seed | 42 | 可复现 |
| save_every_n_epochs | 10 | 每 10 epoch 存 checkpoint12 个) |
| sample_every_n_epochs | 10 | 每 10 epoch 出 4 张样本图 |
| 分辨率 | 1024 | |
**云端 GPU****RTX 4090 Secure Cloud $0.74/h**A40 无容量,最后用 4090 成功)
**预计时长**3840 步 × ~3.3s/步 ≈ **3.5-4 小时**,花费 **~$3**
---
## 三、云端 pod 全流程(v2 实测验证,脚本化)
### 3.1 创建 podRunPod REST API
```powershell
$proj = "D:\F\NewI\opencode\daily-workspace\projects\脸部LoRA训练-Qwen-Image"
$key = Get-Content "$proj\.runpod_api_key"
$body = @{
name = "face-lora-train-v3"
imageName = "runpod/pytorch:2.4.0-py3.11-cuda12.4.1-devel-ubuntu22.04"
gpuTypeIds = @("NVIDIA GeForce RTX 4090")
gpuCount = 1
cloudType = "SECURE" # 铁律:只用 Secure,禁 Community
containerDiskInGb = 100
volumeInGb = 0
} | ConvertTo-Json
$r = Invoke-RestMethod -Uri "https://rest.runpod.io/v1/pods" -Method Post `
-Headers @{Authorization = "Bearer $key"; "Content-Type" = "application/json"} -Body $body -TimeoutSec 60
"pod id: $($r.id) | cost/h: $($r.costPerHr)"
```
**关键点**
- **API key 位置**`$proj\.runpod_api_key``C:\Users\hmo\.runpod_api_key` 不存在,别用错)
- **A40 曾无容量**stuck provisioning)→ 4090 是最终验证可行的
- 创建后轮询 pod 状态直到 `runtime` 非空(拿到 IP + SSH 端口 + Jupyter 端口 + 密码)
### 3.2 等待就绪 + 拿连接信息
```powershell
Start-Sleep 60 # 后台等待(铁律:禁止前台 sleep 轮询,分两步)
$r = Invoke-RestMethod -Uri "https://rest.runpod.io/v1/pods/$PODID" -Headers @{Authorization = "Bearer $key"} -TimeoutSec 30
if ($r.runtime) {
"publicIp: $($r.runtime.publicIp)"
$r.runtime.ports | ForEach-Object { "private:$($_.privatePort) -> public:$($_.publicPort)" }
"jupyterPass: $($r.runtime.jupyterPassword)"
}
```
### 3.3 ⚠️ SSH key 修复(每次新 pod 必做,最大的坑)
**背景**RunPod Settings 里注册的 key 是错误的(指纹 `I2i//`,与本地私钥 `7Eep5Qz` 不匹配)。新 pod 的 `PUBLIC_KEY` 环境变量注入的是错误 key → SSH 永远 Permission denied。
**修复**(需用户在 RunPod Web Terminal 手动执行一次):
```bash
curl -sk -u xiaoxiao:fXmRReiHMnY3AVB "https://git.yoin.fun/api/v1/repos/xiaoxiao/face-lora-qwen-image/raw/cloud/correct_key.pub" -o /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys && service ssh restart && ssh-keygen -lf /root/.ssh/authorized_keys
```
成功标志:指纹显示 `7Eep5QzBfPDzvTgHfNMOC5TPOmW+zSbhHYLctKmkX5Y`(与本地 `id_rsa.pub` 一致)。
**正确 key 已存**`cloud/correct_key.pub`Gitea 仓库里也有,可 curl 拉取)。
### 3.4 上传 4 个文件 + 解压 + 启动
```bash
# 本地 scp 上传(注意:目标路径含中文会坑,用 ASCII 中转或分步)
scp -i $KEY -P $PORT cloud/dataset.zip cloud/bootstrap.sh cloud/train.sh cloud/sample_prompts.txt root@$IP:/workspace/
# SSH 进 pod 后:
cd /workspace
unzip dataset.zip -d train_dataset # pod 上可能没有 unzip → 用 python -m zipfile -e 代替
bash bootstrap.sh # 装 musubi + 下 55G 模型 + 预缓存,10-15 分钟
nohup bash train.sh > /workspace/train.log 2>&1 & # 后台训练
```
**⚠️ SSH 后台任务坑**`nohup ... &` 直接跑会因 SSH session 挂起。正确:`setsid nohup bash train.sh > /workspace/train.log 2>&1 < /dev/null &`(三流全重定向)。
### 3.5 checkpoint 下载(HTTP 代理,不用 scp
```bash
# pod 上启动 http.server(替代 Jupyter 占用的 8888
python3 -m http.server 8888 --directory /workspace/ckpt
# 本地 aria2c 多线程下载(验证过:aria2c 1.37.0 可用)
aria2c -x16 -s16 -k1M -d "local_dir" -o "myface_lora-000050.safetensors" "https://$PODID-8888.proxy.runpod.net/myface_lora-000050.safetensors"
```
**下载 URL 格式**`https://<podid>-8888.proxy.runpod.net/<filename>`
**checkpoint 大小**~563MB 每个(v2 实测)
**校验**:完整文件 = 590153736 字节(562.8MB),小于此值 = 不完整,删了重下。
### 3.6 收尾:删除 pod
```powershell
Invoke-RestMethod -Uri "https://rest.runpod.io/v1/pods/$PODID" -Method Delete -Headers @{Authorization = "Bearer $key"} -TimeoutSec 30
```
---
## 四、⚠️ 上次 v2 训练完整翻车复盘(必须吸取)
### 时间线(8/6 23:00 - 8/7 07:55,用户睡觉期间)
1. **23:59** 创建 pod `f0fhlwz7rrhma6`4090 Secure $0.74/h
2. **00:02** 用户 Web Terminal 修 key → SSH 连上 → 上传 → bootstrap
3. **00:10** 训练启动(2400 步,3.3s/步,预计 2.2h
4. **00:23** 用户睡觉。我启动 monitor_v2.ps1SSH 轮询)
5. **00:26** 训练正常:261/2400loss 0.0601
6. **07:54** 用户醒来质问:"你在干什么?"
- **SSH 连不上**banner exchange refused
- **pod 没了**0 pods)→ **余额耗尽,pod 被 RunPod 强制终止**
- monitor 日志 **steps=? 一直解析失败**(bug)→ 没检测到余额耗尽,没抢救
7. **08:00** 确认:**训练其实完整跑完了**samples 48 张 = e010-e120 全有)!但 **checkpoint 一个没下载**monitor 只等 e120 后下载,且下载逻辑没触发)
### 根因(三条,全部是 monitor 脚本 bug
| # | Bug | 后果 |
|---|---|---|
| 1 | `tail -2` 抓 tqdm 进度 → 正则失败,steps 永远 `?` | 进度不可见,异常检测失效 |
| 2 | 下载只在 e120 出现后一次性执行,且用 scp(中文路径坑) | 中途不下载,崩溃时抢救也失败 |
| 3 | **无余额监控** | 余额耗尽 pod 被删,checkpoint 全丢 |
### 其他踩坑(8/7 全天)
- **scp 目标路径含中文**`projects\脸部LoRA训练-Qwen-Image`)→ 文件写入失败。解决:用 HTTP 代理下载
- **ssh 后台任务挂起 session**`nohup &` 不够,要 `setsid nohup ... </dev/null >log 2>&1 &`
- **image_to_text.pySenseNova)用 musubi venv python 跑**(系统 python PATH 坏了)
- **OpenCV 读不了中文路径的 ONNX 模型** → 拷贝到 %TEMP% ASCII 路径
- **GUI 每次改代码要升版本号**(页面标题 + 终端都显示,验证重启生效)
- **前后台铁律**:禁止前台 sleep/轮询;长期任务必须后台
---
## 五、v3 改进方案(本次要用,monitor_v3.ps1 已写好)
### 核心变化:从"SSH 轮询"改为"HTTP 文件轮询 + 状态持久化 + 计划任务"
| 项 | v2(失败) | v3(本次) |
|---|---|---|
| 调度 | 手动 while 循环 | **Windows 计划任务每 5 分钟** + PID 锁 |
| 进度信号 | SSH 解析 tqdm(失败) | **云端 checkpoint 文件出现**HTTP HEAD |
| 下载 | 只等 e120 一次性 | **增量下载**:每个 epoch 出现立即下 |
| 余额 | 无 | **时长止损**:超预算自动删 pod |
| 崩溃 | 依赖进度解析(失效) | checkpoint mtime 超 45 分钟未更新 → 抢救 |
| 状态 | 无 | `monitor_state.txt` 持久化(已下载清单) |
| 通知 | 无 | 只写日志(用户明确不要微信/通知) |
### 监控脚本(正式版)
**`projects/脸部LoRA训练-Qwen-Image/tools/monitor_train_v3.ps1`**(计划任务指向此文件)
-`temp/train_env.json`pod_id/dl_url/ssh_host/ssh_port/max_hours/total_steps
- PID 锁防重(计划任务重复触发安全)
- 每次运行做一轮:列云端 checkpoint → 增量下载 → 完成判定 → 崩溃判定 → 超时止损 → 写状态
- **所有输出在纯 ASCII 目录** `C:\Users\hmo\AppData\Local\Temp\opencode\face_lora\`monitor_v3.log / train_events.log / monitor_state.txt / checkpoints_v3/
### ⚠️ PS 5.1 血泪教训(2026-08-09 调试 40 分钟才定位)
1. **PowerShell 变量名不区分大小写!** `$STATE`(路径)会被 `$state`hashtable)**覆盖** → 状态从未真正持久化 → 超时止损/崩溃检测/已下载清单全部静默失效。**路径变量必须用独特名**(如 `$STATEFILE`)。
2. **中文路径下 Add-Content 写新 .json 文件不可靠**Test-Path 返回 True 但文件实际不存在)→ **输出目录用纯 ASCII,状态用纯文本 .txt**
3. **train_env.json 读取失败必须退出**(曾因 $PROJ 未定义导致配置全空 → TIMEOUT: 0h>budget 差点误删 pod)。已加安全阀:关键字段为空即 exit。
4. **脚本必须存 GBK 编码**PowerShell 5.1 用 ANSI 读),且**不能含非 GBK 字符**(⚠️ \u26a0 等会崩)。
5. **Event 日志**train_events.log)记录关键动作(下载/完成/止损/删pod),未来 session 恢复上下文的依据。
6. **⚠️ 完成判定必须用 final 文件(myface_lora.safetensors)作为信号,不能等 e120**!训练在 e110 后直接存 final**没有 e120 checkpoint**。本次 monitor 用 `hasE120 && hasFinal` 判定 → 永远不触发 → **pod 多烧 12 小时 ~$9**(血泪教训,2026-08-09)。正确:`if ($hasFinal)` 即完成。
7. **⚠️ checkpoint 文件名是 6 位零填充 `{0:D6}`**myface_lora-000010.safetensors),不是 `000$e`(00010)——否则探测全部 404 下载不到。
8. **⚠️ 下载循环每轮只下 1 个 + 跳过本地已完整 + aria2c `--continue`**:否则串行下多个 1.18GB 超时导致 persist 来不及执行、每轮重复下载。
### 需要准备 train_env.json(创建 pod 后填入)
```json
{
"pod_id": "<创建后从API拿>",
"dl_url": "https://<podid>-8888.proxy.runpod.net",
"ssh_host": "<publicIp>",
"ssh_port": <ssh public port>,
"max_hours": 4.0,
"total_steps": 3840
}
```
---
## 六、执行清单(本次训练,按顺序)
- [ ] 1. 用户确认:**批准启动云端训练**(涉及花钱 $3-4)
- [ ] 2. 用 §3.1 API 创建 pod4090 Secure
- [ ] 3. 轮询拿到 IP/SSH 端口/8888 端口
- [ ] 4. **用户 Web Terminal 执行一次** §3.3 的 key 修复命令
- [ ] 5. 上传 4 文件 → 解压 → bootstrapnohup 后台)
- [ ] 6. bootstrap 完成 → 启动 train.shnohup 后台)
- [ ] 7. pod 上启动 http.server 8888 服务 ckpt 目录
- [ ] 8. 填好 train_env.json → 注册 Windows 计划任务(每 5 分钟跑 monitor_v3.ps1
- [ ] 9. 用户睡觉。监控全自动:增量下载 checkpoint → 完成/崩溃/超时自动处理
- [ ] 10. 早上验证:本地 checkpoints_v3 目录应有一批 563MB 完整 checkpoint
- [ ] 11. checkpoint 复制到 WebUI `models\Lora\qwen\` → Rapid-AIO 出图验证
---
## 七、关键文件位置
| 文件 | 路径 |
|---|---|
| 训练集 | `K:\AI\training\ldf\singled\5. autohandling_v6\train_dataset\` |
| 数据集 zip | `cloud/dataset.zip` |
| 云端 bootstrap | `cloud/bootstrap.sh` |
| 云端训练 | `cloud/train.sh`(已加崩溃自动重启 ×20 |
| 正确 SSH key | `cloud/correct_key.pub` |
| API key | `projects\脸部LoRA训练-Qwen-Image\.runpod_api_key` |
| 监控 v3 | `temp/monitor_v3.ps1` |
| 监控配置 | `temp/train_env.json`(待创建 pod 后填) |
| 监控日志 | `temp/monitor_v3.log` |
| 本地 checkpoint | `temp/checkpoints_v3/` |
---
## 八、铁律(每次训练必须遵守)
1. **只用 Secure Cloud**,禁 Community(用户明确禁止)
2. **创建 pod = 花钱**,必须用户批准后才能执行
3. **训练期间零手动干预**,一切自动(用户睡觉)
4. **任何新操作先查本文档**,不记得就问,不猜
5. **改代码必须升版本号**(页面+终端可见)
6. **禁止前台 sleep/轮询**;长期任务后台启动
7. **scp 不用中文路径**;下载用 HTTP 代理 + aria2c
8. **checkpoint 增量下载**,任何时刻断线已下载的都是安全的
9. **余额/时长超预算自动止损**,不裸奔
10. **训练完成后自动删 pod**,停计费
11. **⚠️ 换 GPU 前先查架构兼容性**:镜像 PyTorch 2.4 支持 CUDA sm_50-905090(sm_120) 不可用,4090(sm_89)/A40(sm_86) 可用
12. **⚠️ 预缓存验证看 `_qie.safetensors`**(数量=素材数),不是 .npz;确认缓存齐全再启动训练,否则报 "No training items found"
13. **文档更新不用问用户**,改完直接写;涉及关键决策(换 GPU/删 pod)才需要确认
14. **每次换 pod 都要用户修 SSH key**RunPod 注册 key 是坏的 I2i//)——除非找到一次性注入正确 key 的方法
---
## 本次 v4 训练实况(2026-08-10,续炼)
| 项 | 值 |
|---|---|
| pod id | 74oru7saetzjo9RTX 4090 Secure0.74/h |
| 类型 | **续炼**(非从头) |
| 续炼起点 | myface_lora-000060.safetensorsv3 成果,1.1GB |
| 数据集 | v732 图 + 全面重调 caption,3 张白色长手套标注) |
| 训练 | dim32/lr1e-4/**50ep**/32张=1600步,实测 ~3.2s/步,~1h15m |
| 预缓存 | 云端 bootstrap 内完成(Vae + TextEncoder |
| 输出 | myface_v7000010-000040 + final,各 1125MB |
| 完成 | final 下载 → 自动删 pod06:15:35),花费 ~2h ≈ .5 |
| 成果位置 | WebUI models\Lora\qwen\ivy\v7\5 文件,全 1125MB 完整) |
### v4 新增经验
1. **续炼流程**:本地打包 v7 dataset.zip → train.sh 加 --network_weights 指旧 checkpoint → 上传旧 checkpoint 到 /workspace/ → bootstrap 里 [3.5] 步骤自动移到 /workspace/ckpt/
2. **运行新 pod 后查连接信息**RunPod API 的
untime 字段为空是正常的!连接信息在**顶层**字段(publicIp/portMappings/ports),别等
untime 就绪(会误判卡住)
3. **SSH key**:本次 env.PUBLIC_KEY 显示正确 keyhmo@daily-workspace),但 SSH 仍 Permission denied → 仍需用户 Web Terminal 执行 §3.3 修复命令(指纹 7Eep5Qz
4. **训练完成无最后中间档**50ep 训练最终只有 000010-000040 + finalepoch 50 直接存 final,无 000050)——monitor 别等最后中间档,final 出现即完成(与 v3 的 e110 模式一致)
5. **monitor 状态残留坑**:旧训练 monitor_state.txt 的 done=True 会让新监控直接跳过 → 每次新训练前必须重置状态文件
6. **monitor 变量名坑**:不能用 $envPS 内置 provider 前缀)当普通变量,会导致状态持久化静默失效
7. **本地 run_cache.py 是死代码**:训练走云端,本地预缓存脚本无意义(曾误花时间修它)
### 成果对比建议
+38
View File
@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
"""预缓存:VAE latent + Text Encoder 输出(冒烟测试数据集)"""
import os, subprocess, sys
os.environ["PYTHONUTF8"] = "1"
os.environ["HF_HUB_OFFLINE"] = "1"
VENV = r"M:\AI\sd\musubi-tuner\.venv\Scripts"
PY = os.path.join(VENV, "python.exe")
MUSUBI = r"M:\AI\sd\musubi-tuner" # musubi-tuner 根目录(cwd 必须在此,musubi_tuner 包才能导入)
SRC = os.path.join(MUSUBI, "src", "musubi_tuner")
PROJ = r"D:\F\NewI\opencode\daily-workspace\projects\脸部LoRA训练-Qwen-Image"
MODELS = r"M:\AI\sd\models\qwen-edit-2511"
CFG = r"D:\F\NewI\opencode\daily-workspace\projects\脸部LoRA训练-Qwen-Image\config\dataset_active.toml"
VAE = os.path.join(MODELS, "diffusion_pytorch_model.safetensors")
TE = os.path.join(MODELS, "text_encoder", "model-00001-of-00004.safetensors")
steps = [
("VAE latent 缓存", [PY, os.path.join(SRC, "qwen_image_cache_latents.py"),
"--dataset_config", CFG, "--vae", VAE, "--device", "cuda", "--skip_existing",
"--model_version", "edit-2511"]),
("TextEncoder 缓存", [PY, os.path.join(SRC, "qwen_image_cache_text_encoder_outputs.py"),
"--dataset_config", CFG, "--text_encoder", TE, "--fp8_vl", "--device", "cuda", "--skip_existing",
"--model_version", "edit-2511"]),
]
for name, cmd in steps:
print(f"\n===== {name} =====")
print(" ".join(cmd))
# cwd=musubi-tuner 根目录:qwen_image_cache_*.py 内部 from musubi_tuner.dataset import config_utils 才能找到包
r = subprocess.run(cmd, cwd=MUSUBI)
if r.returncode != 0:
print(f"[FAIL] {name} exit={r.returncode}")
sys.exit(r.returncode)
print(f"[OK] {name}")
print("\n全部缓存完成")
+2224
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+305
View File
@@ -0,0 +1,305 @@
# -*- coding: utf-8 -*-
"""
merge_fixed.py — 原图 + AI修改图 无缝融合工具
============================================
背景:ComfyUI inpainting 移除合影中的其他人物时,即使指定"非修改区域不得像素漂移"
全图清晰度仍不可避免损失。本工具把:
- 原图的"未修改区域"(清晰度完好)
- 修改图的"修改区域"(人物被干净移除)
自动合并,过渡自然无痕迹。
用法:
python merge_fixed.py <原图目录> <修改图目录> <输出目录> [--feather 25] [--threshold 20]
匹配规则:
- 原图 IMG_0463.jpg(任意后缀 jpg/png/webp/jpeg
- 修改图 IMG_0463*.png(以原图文件名开头、.png 结尾、中间任意字符)
- 一张原图可有多张修改图,每张都独立合并
- 输出:{原图stem}_{修改图中间部分}.png
算法:
1. 修改图 resize 到原图尺寸
2. Lab 色彩空间差异图 → 高斯模糊去噪 → 阈值 → 闭运算+膨胀 = 修改区 mask
3. 羽化混合:result = 原图*(1-alpha) + 修改图*alphaalpha 由 mask 高斯模糊得到)
"""
import argparse
import os
import sys
from pathlib import Path
import cv2
import numpy as np
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
# 复用 face_checker 的人脸检测(目标人物面部保护)
sys.path.insert(0, str(Path(__file__).resolve().parent))
import face_checker as fc
IMG_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp"}
_fc_det = None
def get_face_detector():
global _fc_det
if _fc_det is None:
_fc_det = fc.FaceDetector()
return _fc_det
def protect_faces(alpha, mod_rgb, expand=1.35, margin=40):
"""
面部强制保护(保险):检测修改图中残留的人脸(=目标人物,被移除者已被 inpaint 掉),
把这些人脸区域的 alpha 置 0(100% 用原图像素,零改变)。
通用规则(有限修改→原图)已覆盖大部分,此项兜底确保面部绝对不变。
"""
det = get_face_detector()
faces = fc._detect_faces_fast(mod_rgb, det)
if not faces:
return alpha
for f in faces:
x, y, w, h = f[0], f[1], f[2], f[3]
cw, ch = w * expand, h * expand
x0 = max(0, int(x - (cw - w) / 2) - margin)
y0 = max(0, int(y - (ch - h) / 2) - margin)
x1 = min(alpha.shape[1], int(x + w + (cw - w) / 2) + margin)
y1 = min(alpha.shape[0], int(y + h + (ch - h) / 2) + margin)
alpha[y0:y1, x0:x1] = 0.0
return alpha
def build_alpha_from_mask(mask_rgb, feather=20):
"""
从 ComfyUI inpaint 蒙版生成 alpha:白色区域 = 被修改区(100% 修改图),黑色 = 原图。
蒙版是最准确的"修改区"标记(用户在 ComfyUI 画的就是要移除的人物),
比 diff 检测可靠 100 倍——紧贴人物的残影完美解决。
feather: 蒙版边缘羽化像素
"""
gray = cv2.cvtColor(mask_rgb, cv2.COLOR_RGB2GRAY).astype(np.float32) / 255.0
# 蒙版白色→1(修改图),黑色→0(原图)
alpha = gray
if feather > 0:
k = feather * 2 + 1
alpha = cv2.GaussianBlur(alpha, (k, k), 0)
return alpha
def find_mask(orig_path, mask_dir):
"""按文件名匹配蒙版:{stem}*mask*.png 或 {stem}*.png(取 mask 关键字优先)"""
stem = Path(orig_path).stem
cands = sorted(p for p in mask_dir.iterdir()
if p.suffix.lower() == ".png" and p.name.startswith(stem))
# 优先带 mask 关键字的
for p in cands:
if "mask" in p.name.lower():
return p
return cands[0] if cands else None
def build_alpha(orig_rgb, mod_rgb, blur_kernel=15, threshold=4.0, min_area_ratio=0.005,
softness=1.5, dilate=60, body_extend=2.2):
"""
融合权重 alpha(0=原图像素, 1=修改图像素)——双图人脸差集定位被移除人物:
1. 原图人脸 - 修改图人脸 = 被移除人物(合影中被清掉的人)
2. 被移除人物区域(脸框 + 向下 body_extend 倍覆盖身体)= 修改区
3. 目标人物(修改图残留的最大人脸)紧贴框 = 保留区(强制原图)
4. diff 高值连通块(排除保留区)兜底其他修改
5. 膨胀覆盖边缘残影
紧贴合影场景:被移除人物与目标紧贴时,靠"人脸差集"精确定位,不靠 diff 幅度。
"""
det = get_face_detector()
orig_faces = fc._detect_faces_fast(orig_rgb, det)
mod_faces = fc._detect_faces_fast(mod_rgb, det)
h, w = orig_rgb.shape[:2]
def matched(of, mfaces):
ox, oy = of[0] + of[2] / 2, of[1] + of[3] / 2
return any(abs(ox - (mf[0] + mf[2] / 2)) < of[2] * 0.8 and abs(oy - (mf[1] + mf[3] / 2)) < of[3] * 0.8
for mf in mfaces)
removed_faces = [f for f in orig_faces if not matched(f, mod_faces)]
# 过滤误检小脸(背景物体/花纹被当脸):只保留足够大的人脸
# 真实人物脸 ≥ 图面积 0.05% 或短边 ≥ 40px
min_face_area = max(1, int(h * w * 0.0005))
removed_faces = [f for f in removed_faces
if f[2] * f[3] >= min_face_area and min(f[2], f[3]) >= 40]
# 被移除人物区(脸 + 身体向下扩展)
mod_mask = np.zeros((h, w), np.uint8)
for f in removed_faces:
x, y, fw, fh = [int(v) for v in f[:4]]
x0 = max(0, int(x - fw * 0.4)); x1 = min(w, int(x + fw * 1.4))
y0 = max(0, int(y - fh * 0.3)); y1 = min(h, int(y + fh * body_extend))
mod_mask[y0:y1, x0:x1] = 1
# 保留区:目标人物最大脸,紧贴框(左右不扩,防侵入紧贴的被移除人物)
keep_mask = np.zeros((h, w), np.uint8)
if mod_faces:
big = max(mod_faces, key=lambda f: f[2] * f[3])
x, y, fw, fh = [int(v) for v in big[:4]]
x0, x1 = max(0, x), min(w, x + fw)
y0, y1 = max(0, int(y - fh * 0.35)), min(h, int(y + fh * 1.2))
keep_mask[y0:y1, x0:x1] = 1
# diff 兜底连通块(排除保留区)。无被移除人物时用高阈值(保守,防降质误判)
o_lab = cv2.cvtColor(orig_rgb, cv2.COLOR_RGB2LAB).astype(np.float32)
m_lab = cv2.cvtColor(mod_rgb, cv2.COLOR_RGB2LAB).astype(np.float32)
diff_s = cv2.GaussianBlur(np.abs(o_lab - m_lab).mean(axis=2), (blur_kernel, blur_kernel), 0)
eff_threshold = threshold if removed_faces else max(threshold, 15.0)
base = ((diff_s > eff_threshold) & ~keep_mask.astype(bool)).astype(np.uint8)
num, labels, stats, _ = cv2.connectedComponentsWithStats(base, 8)
min_area = max(1, int(min_area_ratio * h * w))
filtered = np.zeros_like(base)
for i in range(1, num):
if stats[i, cv2.CC_STAT_AREA] >= min_area:
filtered[labels == i] = 1
core = np.maximum(filtered, mod_mask)
if dilate > 0:
core = cv2.dilate(core, np.ones((dilate, dilate), np.uint8))
alpha = core.astype(np.float32)
if softness > 0:
k = int(softness * 6) * 2 + 1
alpha = cv2.GaussianBlur(alpha, (k, k), 0)
return alpha
"""
融合权重 alpha(0=原图像素, 1=修改图像素):
- 固定阈值判定明显修改区(人物移除 diff 高,有限降质 diff 低,区分度 ~20x)
- diff > 阈值 → alpha≈1100% 修改图,修改区内部零残影)
- diff < 阈值 → alpha≈0100% 原图,目标人物/有限修改区零改变)
- 修改区膨胀 dilate 像素(默认 75):覆盖人物边缘 10-40px 的浅残影带
(残影带 diff 接近未修改区,无法用阈值检测,必须靠膨胀)
- 陡 sigmoid 边缘过渡防硬边
"""
o = cv2.cvtColor(orig_rgb, cv2.COLOR_RGB2LAB).astype(np.float32)
m = cv2.cvtColor(mod_rgb, cv2.COLOR_RGB2LAB).astype(np.float32)
diff = np.abs(o - m).mean(axis=2)
diff_s = cv2.GaussianBlur(diff, (blur_kernel, blur_kernel), 0)
alpha = 1.0 / (1.0 + np.exp(-(diff_s - threshold) / softness))
# 修改区膨胀:覆盖人物边缘残影带
if dilate > 0:
kernel = np.ones((dilate, dilate), np.uint8)
core = (alpha > 0.5).astype(np.uint8)
core = cv2.dilate(core, kernel)
alpha = np.maximum(alpha, core.astype(np.float32))
return alpha
def seamless_merge(orig_rgb, mod_rgb, alpha):
"""
连续 alpha 混合:result = orig*(1-alpha) + mod*alpha。
alpha 已是平滑的浮点图(sigmoid 过渡),无需额外羽化。
"""
a = alpha[..., None].astype(np.float32)
result = orig_rgb.astype(np.float32) * (1.0 - a) + mod_rgb.astype(np.float32) * a
return np.clip(result, 0, 255).astype(np.uint8)
def find_modified(orig_path, mod_dir):
"""按文件名匹配修改图:{stem}*.png"""
stem = Path(orig_path).stem
return sorted(p for p in mod_dir.iterdir()
if p.suffix.lower() == ".png" and p.name.startswith(stem))
def process(orig_dir, mod_dir, out_dir, alpha_threshold=8.0, softness=1.5, protect=True,
mask_dir=None, feather=20, verbose=True):
"""
alpha_threshold: 无蒙版时的 diff 阈值(默认 8;误判时调)
softness: 过渡带宽度
protect: 面部强制保护(无蒙版 fallback 时的保险)
mask_dir: ComfyUI inpaint 蒙版目录(可选,有则优先用,蒙版白色=被移除区,最准确)
feather: 蒙版边缘羽化
"""
orig_dir, mod_dir, out_dir = Path(orig_dir), Path(mod_dir), Path(out_dir)
if mask_dir:
mask_dir = Path(mask_dir)
out_dir.mkdir(parents=True, exist_ok=True)
originals = sorted(p for p in orig_dir.iterdir()
if p.suffix.lower() in IMG_EXTS and not p.name.startswith("."))
if not originals:
print(f"[WARN] 原图目录没有图片: {orig_dir}")
return
total = 0
for op in originals:
mods = find_modified(op, mod_dir)
if not mods:
if verbose:
print(f"[跳过] {op.name}: 无匹配修改图")
continue
from PIL import Image
orig_img = Image.open(op).convert("RGB")
ow, oh = orig_img.size
orig_rgb = np.array(orig_img)
# 尝试匹配蒙版
mask_path = find_mask(op, mask_dir) if mask_dir else None
for mp in mods:
try:
mod_img = Image.open(mp).convert("RGB")
if mod_img.size != (ow, oh):
mod_img = mod_img.resize((ow, oh), Image.LANCZOS)
mod_rgb = np.array(mod_img)
if mask_path and mask_path.exists():
# 蒙版模式:最准确
mask_img = Image.open(mask_path).convert("RGB")
if mask_img.size != (ow, oh):
mask_img = mask_img.resize((ow, oh), Image.LANCZOS)
mask_rgb = np.array(mask_img)
alpha = build_alpha_from_mask(mask_rgb, feather=feather)
mode = f"蒙版模式({mask_path.name})"
else:
# 双图人脸差集模式(自动定位被移除人物 + 保留目标脸)
alpha = build_alpha(orig_rgb, mod_rgb, threshold=alpha_threshold, softness=softness)
mode = "差集模式"
area_ratio = float((alpha > 0.5).mean())
if area_ratio < 0.0005:
if verbose:
print(f"[警告] {op.name} <- {mp.name}: 明显修改区仅 {area_ratio*100:.2f}%%,可能未检测到修改")
mask_area_note = f"明显修改区 {area_ratio*100:.2f}% (偏小?)"
else:
mask_area_note = f"明显修改区 {area_ratio*100:.1f}%"
result = seamless_merge(orig_rgb, mod_rgb, alpha)
mid = mp.stem[len(Path(op).stem):] or "_mod"
out_name = f"{Path(op).stem}{mid}.png"
Image.fromarray(result).save(out_dir / out_name)
total += 1
if verbose:
print(f"[OK] {out_name} | {mode} | {mask_area_note}")
except Exception as e:
if verbose:
print(f"[失败] {op.name} <- {mp.name}: {e}")
print(f"\n完成:共输出 {total} 张融合图 -> {out_dir}")
def main():
ap = argparse.ArgumentParser(description="原图 + AI修改图 无缝融合工具(支持 ComfyUI 蒙版)")
ap.add_argument("orig_dir", help="原图目录")
ap.add_argument("mod_dir", help="修改图目录(文件名以原图名开头、.png 结尾)")
ap.add_argument("out_dir", help="输出目录")
ap.add_argument("--mask-dir", default=None,
help="ComfyUI inpaint 蒙版目录(可选,有则优先用;蒙版白色=被移除区,最准确解决紧贴残影)")
ap.add_argument("--alpha-threshold", type=float, default=8.0,
help="无蒙版时的 diff 阈值(默认 8")
ap.add_argument("--softness", type=float, default=1.5, help="过渡带宽度(默认 1.5")
ap.add_argument("--feather", type=int, default=20, help="蒙版边缘羽化(默认 20")
ap.add_argument("--no-protect-face", action="store_true", help="关闭面部强制保护(无蒙版时的保险)")
args = ap.parse_args()
process(args.orig_dir, args.mod_dir, args.out_dir,
alpha_threshold=args.alpha_threshold, softness=args.softness,
protect=not args.no_protect_face, mask_dir=args.mask_dir, feather=args.feather)
if __name__ == "__main__":
main()
Binary file not shown.
+236
View File
@@ -0,0 +1,236 @@
# FaceLoRA v3 training monitor (scheduled task, fully autonomous)
# ============================================================
# 用途:云端训练全自动监控。Windows 计划任务每 5 分钟调用一次。
# 功能:
# 1. 增量下载 checkpointHTTP HEAD 探测 + aria2c)——任何时刻断线已下载的都是安全的
# 2. 完成判定:e120 + final 都下载 -> 删 pod 停计费
# 3. 崩溃检测:最后 checkpoint 45min 无更新 -> 抢救 + 删 pod
# 4. 超时止损:超过预算小时数 -> 删 pod
# 5. 状态持久化到 monitor_state.txt(纯文本,PS5.1 下 .json 写入不可靠)
# 输出(全在 ASCII 目录 C:\Users\hmo\AppData\Local\Temp\opencode\face_lora\):
# monitor_v3.log - 详细运行日志
# train_events.log - 关键事件时间线(下载/完成/止损/删pod)
# monitor_state.txt - 持久化状态(pod_start/downloaded/done/started
# checkpoints_v3/ - 已下载的 checkpoint
#
# !! PS 5.1 铁律(血泪教训,2026-08-09):
# - 变量名不区分大小写!$STATE(路径) 会被 $state(hashtable) 覆盖 → 路径变量必须用独特名($STATEFILE)
# - 中文路径下 Add-Content 写新 .json 文件不可靠 → 输出目录用纯 ASCII,状态用纯文本
# - train_env.json 读取失败必须退出,绝不用空值执行删 pod(曾因此差点误删)
# ============================================================
$ErrorActionPreference = "Continue"
# ---- 项目路径(注意:脚本必须存 GBK 编码,PowerShell 5.1 用 ANSI 读)----
$PROJ = "D:\F\NewI\opencode\daily-workspace\projects\脸部LoRA训练-Qwen-Image"
# 输出目录用纯 ASCII(避开中文路径 + PS5.1 编码 bug
$OUTDIR = "C:\Users\hmo\AppData\Local\Temp\opencode\face_lora"
# ---- 读取训练环境配置(train_env.jsonpod_id/dl_url/ssh/max_hours/total_steps----
$CFG = Join-Path $PROJ "temp\train_env.json"
if (-not (Test-Path $CFG)) {
Write-Host "missing train_env.json"
exit 1
}
$env_cfg = Get-Content $CFG -Raw | ConvertFrom-Json
$PODID = $env_cfg.pod_id
$DLURL = $env_cfg.dl_url
$SSHHOST = $env_cfg.ssh_host
$SSHPORT = $env_cfg.ssh_port
$MAX_HOURS = [double]$env_cfg.max_hours
$TOTAL_STEPS = [int]$env_cfg.total_steps
# 安全阀:任何关键字段为空 → 立即退出,绝不用空值执行删 pod 等破坏性操作
if (-not $PODID -or -not $DLURL -or $MAX_HOURS -le 0) {
Write-Host ("train_env invalid: pod=" + $PODID + " max_hours=" + $MAX_HOURS)
exit 1
}
$MLOG = Join-Path $OUTDIR "monitor_v3.log"
$EVENT = Join-Path $OUTDIR "train_events.log"
$CDIR = Join-Path $OUTDIR "checkpoints_v3"
$STATEFILE = Join-Path $OUTDIR "monitor_state.txt"
$RPKEY = (Get-Content (Join-Path $PROJ ".runpod_api_key") -Raw).Trim()
New-Item -ItemType Directory -Force $CDIR, (Split-Path $MLOG) | Out-Null
# ---- 单例守卫(防计划任务重复触发)----
$LOCK = Join-Path $OUTDIR "monitor_v3.lock"
if (Test-Path $LOCK) {
$oldPid = [int](Get-Content $LOCK)
if (Get-Process -Id $oldPid -ErrorAction SilentlyContinue) {
exit 0
}
}
Set-Content $LOCK $PID
function Log($msg) {
$ts = Get-Date -Format "MM-dd HH:mm:ss"
Add-Content -Path $MLOG -Value ("[" + $ts + "] " + $msg) -Encoding UTF8
}
function Event($msg) {
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Add-Content -Path $EVENT -Value ("[" + $ts + "] " + $msg) -Encoding UTF8
}
try {
# ---- 读持久化状态(纯文本)----
$state = @{ downloaded = @(); pod_start = ""; done = $false; started = $false }
if (Test-Path $STATEFILE) {
try {
foreach ($ln in Get-Content $STATEFILE) {
if ($ln -like "downloaded=*") { $state.downloaded = @($ln.Substring(11).Split(";") | Where-Object { $_ }) }
elseif ($ln -like "pod_start=*") { $state.pod_start = $ln.Substring(10) }
elseif ($ln -eq "done=true") { $state.done = $true }
elseif ($ln -eq "started=true") { $state.started = $true }
}
} catch {}
}
if (-not $state.pod_start) {
$state.pod_start = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Event ("monitor start: pod=" + $PODID + " dl_url=" + $DLURL + " max_hours=" + $MAX_HOURS)
}
if ($state.done) {
Log "done, skip"
exit 0
}
# ---- 0. 探测训练是否已开始(train.log 有 steps----
if (-not $state.started) {
try {
$tail = & ssh -i "$env:USERPROFILE\.ssh\id_rsa" -p $SSHPORT -o BatchMode=yes -o ConnectTimeout=10 $SSHHOST "tail -c 2000 /workspace/train.log 2>/dev/null | tr '\r' '\n' | tail -2" 2>$null
if ($tail -match ("(\d+)/" + $TOTAL_STEPS)) {
$state.started = $true
Event ("training started: step " + $Matches[1] + "/" + $TOTAL_STEPS)
}
} catch {}
}
# ---- 1. 探测云端 checkpointHTTP HEAD----
$epochs = @(10,20,30,40,50,60,70,80,90,100,110,120)
$newFiles = @()
foreach ($e in $epochs) {
$fn = "myface_lora-{0:D6}.safetensors" -f $e
if ($fn -in $state.downloaded) { continue }
try {
$resp = Invoke-WebRequest -Uri ($DLURL + "/" + $fn) -Method Head -TimeoutSec 15 -UseBasicParsing
if ($resp.StatusCode -eq 200) { $newFiles += $fn }
} catch {}
}
try {
$resp = Invoke-WebRequest -Uri ($DLURL + "/myface_lora.safetensors") -Method Head -TimeoutSec 15 -UseBasicParsing
if ($resp.StatusCode -eq 200) { $newFiles += "myface_lora.safetensors" }
} catch {}
# ---- 2. 增量下载 ----
foreach ($f in $newFiles) {
$fp = Join-Path $CDIR $f
if (Test-Path $fp) {
$sz = [math]::Round((Get-Item $fp).Length/1MB,0)
if ($sz -gt 100) {
Log ("already have: " + $f + " (" + $sz + " MB), skip")
$state.downloaded += $f
continue
}
Remove-Item $fp -Force -ErrorAction SilentlyContinue
}
Log ("new checkpoint: " + $f + " downloading")
& aria2c -x16 -s16 -k1M --continue -d "$CDIR" -o $f ($DLURL + "/" + $f) 2>&1 | Out-Null
if (Test-Path $fp) {
$sz = [math]::Round((Get-Item $fp).Length/1MB,0)
if ($sz -gt 100) {
Log ("download OK: " + $f + " (" + $sz + " MB)")
Event ("CHECKPOINT DOWNLOADED: " + $f + " (" + $sz + " MB)")
$state.downloaded += $f
} else {
Remove-Item $fp -Force -ErrorAction SilentlyContinue
Log ("incomplete (" + $sz + " MB), retry next round: " + $f)
}
} else {
Log ("download FAIL: " + $f)
}
break
}
# ---- 3. 完成判定 ----
$hasFinal = "myface_lora.safetensors" -in $state.downloaded
if ($hasFinal) {
Log "=== TRAINING COMPLETE: final checkpoint downloaded ==="
Event ("=== TRAINING COMPLETE: all checkpoints in checkpoints_v3\ ===")
try {
Invoke-RestMethod -Uri ("https://rest.runpod.io/v1/pods/" + $PODID) -Method Delete -Headers @{Authorization = "Bearer $RPKEY"} -TimeoutSec 30 | Out-Null
Log ("pod " + $PODID + " deleted (complete)")
Event ("POD DELETED: " + $PODID + " (complete, billing stopped)")
} catch { Log ("pod delete fail: " + $_.Exception.Message) }
$state.done = $true
}
# ---- 4. 崩溃检测 ----
if ($state.downloaded.Count -gt 0 -and -not $state.done) {
$lastFile = $state.downloaded[-1]
$lastPath = Join-Path $CDIR $lastFile
if (Test-Path $lastPath) {
$ageMin = [int]((Get-Date) - (Get-Item $lastPath).LastWriteTime).TotalMinutes
if ($ageMin -gt 45) {
Log ("CRASH: last ckpt (" + $lastFile + ") " + $ageMin + "min stale -> rescue + delete pod")
Event ("CRASH: last ckpt " + $lastFile + " " + $ageMin + "min stale -> rescue + delete pod")
foreach ($e in $epochs) {
$fn = "myface_lora-{0:D6}.safetensors" -f $e
if ($fn -in $state.downloaded) { continue }
try {
$r = Invoke-WebRequest -Uri ($DLURL + "/" + $fn) -Method Head -TimeoutSec 15 -UseBasicParsing
if ($r.StatusCode -eq 200) {
& aria2c -x16 -s16 -k1M -d "$CDIR" -o $fn ($DLURL + "/" + $fn) 2>&1 | Out-Null
if (Test-Path (Join-Path $CDIR $fn)) { $state.downloaded += $fn; Event ("RESCUED: " + $fn) }
}
} catch {}
}
try {
Invoke-RestMethod -Uri ("https://rest.runpod.io/v1/pods/" + $PODID) -Method Delete -Headers @{Authorization = "Bearer $RPKEY"} -TimeoutSec 30 | Out-Null
Log ("pod " + $PODID + " deleted (crash)")
Event ("POD DELETED: " + $PODID + " (crash stop)")
} catch { Log ("pod delete fail: " + $_.Exception.Message) }
$state.done = $true
}
}
}
# ---- 5. 超时止损 ----
if (-not $state.done) {
try {
$start = [datetime]::ParseExact($state.pod_start, "yyyy-MM-dd HH:mm:ss", $null)
$runHours = ((Get-Date) - $start).TotalHours
if ($runHours -gt $MAX_HOURS) {
Log ("TIMEOUT: " + [math]::Round($runHours,1) + "h > budget " + $MAX_HOURS + "h")
Event ("TIMEOUT: " + [math]::Round($runHours,1) + "h > budget " + $MAX_HOURS + "h -> delete pod")
try {
Invoke-RestMethod -Uri ("https://rest.runpod.io/v1/pods/" + $PODID) -Method Delete -Headers @{Authorization = "Bearer $RPKEY"} -TimeoutSec 30 | Out-Null
Log ("pod " + $PODID + " deleted (timeout, downloaded " + $state.downloaded.Count + ")")
Event ("POD DELETED: " + $PODID + " (timeout, downloaded " + $state.downloaded.Count + ")")
} catch { Log ("pod delete fail: " + $_.Exception.Message) }
$state.done = $true
} else {
Log ("running: " + [math]::Round($runHours,1) + "/" + $MAX_HOURS + "h, downloaded " + $state.downloaded.Count)
}
} catch {
Log ("time parse fail: " + $_.Exception.Message)
}
}
# ---- 持久化状态(纯文本行,PS5.1 可靠)----
try {
$lines = @()
$lines += "pod_start=" + $state.pod_start
$lines += "downloaded=" + ($state.downloaded -join ";")
$lines += ("done=" + $state.done)
$lines += ("started=" + $state.started)
if (Test-Path $STATEFILE) { Remove-Item $STATEFILE -Force }
Add-Content -Path $STATEFILE -Value $lines -Encoding UTF8
} catch {
Log ("state write fail: " + $_.Exception.Message)
}
$state.downloaded | ForEach-Object { Event ("state: downloaded " + $_) }
if ($state.done) { Event ("=== MONITOR DONE: " + $state.downloaded.Count + " checkpoints ===") }
else { Event ("state: running, downloaded=" + $state.downloaded.Count) }
if ($state.done) { Log "=== monitor done ===" }
} finally {
Remove-Item $LOCK -Force -ErrorAction SilentlyContinue
}
+93
View File
@@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
"""
LoRA checkpoint 验证工具
========================
加载 edit-2511 底模 + 训练好的 LoRA,用多组提示词生成对比图,
验证 LoRA 是否生效、效果如何。
用法:
python validate_lora.py # 验证最新 checkpoint
python validate_lora.py --ckpt <路径> # 验证指定 checkpoint
python validate_lora.py --all # 验证 output/checkpoints 下所有
"""
import glob
import os
import subprocess
import sys
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
VENV = r"D:\AI\sd\musubi-tuner\.venv\Scripts"
PY = os.path.join(VENV, "python.exe")
SRC = r"D:\AI\sd\musubi-tuner\src\musubi_tuner"
MODELS = r"D:\AI\sd\models\qwen-edit-2511"
PROJ = r"D:\F\NewI\opencode\daily-workspace\projects\脸部LoRA训练-Qwen-Image"
TRIGGER = "lm_face_v1"
CKPT_DIR = os.path.join(PROJ, "output", "checkpoints")
OUT_DIR = os.path.join(PROJ, "output", "验证")
# 验证提示词:同一触发词 + 不同场景,看脸是否稳定一致
PROMPTS = [
("证件照", f"{TRIGGER}, professional headshot, business attire, neutral expression, studio lighting, plain white background"),
("日常", f"{TRIGGER}, natural lifestyle portrait, by a window with soft diffused sunlight, warm tones"),
("户外全身", f"{TRIGGER}, full body shot, standing in a park, natural daylight, casual clothing"),
("半侧脸", f"{TRIGGER}, three-quarter view portrait, soft golden hour light, shallow depth of field"),
]
BASE_CMD = [
PY, os.path.join(SRC, "qwen_image_generate_image.py"),
"--dit", os.path.join(MODELS, "transformer", "diffusion_pytorch_model-00001-of-00005.safetensors"),
"--vae", os.path.join(MODELS, "diffusion_pytorch_model.safetensors"),
"--text_encoder", os.path.join(MODELS, "text_encoder", "model-00001-of-00004.safetensors"),
"--model_version", "edit-2511",
"--fp8", "--fp8_scaled", "--blocks_to_swap", "24",
"--infer_steps", "25",
"--image_size", "1024", "1024",
"--seed", "42",
]
def pick_ckpts(args):
if args.ckpt:
return [args.ckpt]
if args.all and os.path.isdir(CKPT_DIR):
return sorted(glob.glob(os.path.join(CKPT_DIR, "*.safetensors")))
ckpts = sorted(glob.glob(os.path.join(CKPT_DIR, "*.safetensors")))
return [ckpts[-1]] if ckpts else []
def main():
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("--ckpt", default=None, help="指定 checkpoint 路径")
ap.add_argument("--all", action="store_true", help="验证所有 checkpoint")
args = ap.parse_args()
ckpts = pick_ckpts(args)
if not ckpts:
print(f"[WARN] 没有找到 checkpoint: {CKPT_DIR}")
sys.exit(1)
os.makedirs(OUT_DIR, exist_ok=True)
print(f"找到 {len(ckpts)} 个 checkpoint,开始验证(每个 4 张图)...")
for ckpt in ckpts:
name = os.path.splitext(os.path.basename(ckpt))[0]
print(f"\n===== {name} =====")
for tag, prompt in PROMPTS:
out = os.path.join(OUT_DIR, f"{name}_{tag}.png")
cmd = BASE_CMD + ["--lora_weight", ckpt, "--lora_multiplier", "0.8",
"--prompt", prompt, "--save_path", out]
print(f" 生成 [{tag}] ...")
r = subprocess.run(cmd)
if r.returncode == 0:
print(f" OK -> {out}")
else:
print(f" FAIL exit={r.returncode}")
print(f"\n验证完成,图片在 {OUT_DIR}")
if __name__ == "__main__":
main()
+16
View File
@@ -0,0 +1,16 @@
@echo off
chcp 65001 >nul
rem ============================================
rem face_checker 一键审核:把照片放进 photos 文件夹,双击本文件
rem ============================================
set TOOL=D:\F\NewI\opencode\daily-workspace\projects\脸部LoRA训练-Qwen-Image\tools\face_checker.py
set PY=D:\AI\sd\musubi-tuner\.venv\Scripts\python.exe
set PHOTOS=D:\F\NewI\opencode\daily-workspace\projects\脸部LoRA训练-Qwen-Image\photos
if not exist "%PHOTOS%" mkdir "%PHOTOS%"
echo 正在审核 %PHOTOS% ...
"%PY%" "%TOOL%" check "%PHOTOS%"
echo.
echo 报告已生成: %PHOTOS%\素材审核报告.html
start "" "%PHOTOS%\素材审核报告.html"
pause
+16
View File
@@ -0,0 +1,16 @@
@echo off
chcp 65001 >nul
rem ============================================
rem face_checker 智能选图:从 photos 里挑出最多样化的 20 张
rem ============================================
set TOOL=D:\F\NewI\opencode\daily-workspace\projects\脸部LoRA训练-Qwen-Image\tools\face_checker.py
set PY=D:\AI\sd\musubi-tuner\.venv\Scripts\python.exe
set PHOTOS=D:\F\NewI\opencode\daily-workspace\projects\脸部LoRA训练-Qwen-Image\photos
if not exist "%PHOTOS%" mkdir "%PHOTOS%"
echo 正在智能选图(推荐 20 张)...
"%PY%" "%TOOL%" pick "%PHOTOS%" --count 20
echo.
echo 推荐报告已生成: %PHOTOS%\智能选图推荐_20张.html
start "" "%PHOTOS%\智能选图推荐_20张.html"
pause
+73
View File
@@ -0,0 +1,73 @@
# face_checker 使用说明(素材自动审核 + 智能选图工具)
## 这是什么
训练脸部 LoRA 前的**素材质检员 + 选图助手**:
- 自动检查每张照片:分辨率、清晰度、人脸数量、人脸大小、正侧脸角度、遮挡、重复图
- 红黄绿三档判定:✅合格 / 🟡警告 / ❌不合格,附具体原因
- **智能选图**:从一堆照片里自动挑出多样化组合(覆盖特写/半身/全身、不同角度、不同风格),你只需要删掉重复的
## 快速开始
用哪个 Pythonmusubi-tuner 的虚拟环境
```powershell
# 场景一:审核照片,生成报告
& "D:\AI\sd\musubi-tuner\.venv\Scripts\python.exe" "D:\F\NewI\opencode\daily-workspace\projects\脸部LoRA训练-Qwen-Image\tools\face_checker.py" check "照片目录"
# 场景二:智能选图(推荐 20 张)
& "D:\AI\sd\musubi-tuner\.venv\Scripts\python.exe" "...\tools\face_checker.py" pick "照片目录" --count 20
# 场景三:审核 + 把合格照片整理成训练集
& "...\face_checker.py" batch "照片目录" --out "训练集目录"
```
> 提示:可以把工具路径存成 PowerShell 变量或做个小 .bat,避免每次打长路径(我可以帮你做)。
## 输出
| 命令 | 产出 |
|---|---|
| `check` | `素材审核报告.html`(浏览器打开看图片墙+判定)+ `素材审核结果.json` |
| `pick` | `智能选图推荐_N张.html`(推荐清单+构图标签)+ `智能选图结果.json` |
| `batch` | 报告 + 训练集文件夹(img_001.jpg 重命名、EXIF 修正、统一 JPG) |
## 判定规则(快查)
| 检查项 | 合格 | 警告 | 不合格 |
|---|---|---|---|
| 分辨率 | 短边 ≥1024 | - | 短边 <1024(微信压缩图) |
| 清晰度 | 人脸区域锐利 | 偏糊 | 明显模糊 |
| 人脸 | 1 张 | 多张脸 / 占比小 / 侧脸 / 疑似遮挡 | 没检测到脸 |
| 重复 | - | 与另一张高度相似 | - |
## 老莫的操作流程
1. 把候选照片**全部**丢进一个文件夹(几十张也没关系,不用自己先筛)
2.`pick --count 20` → 打开推荐报告看一眼,删掉不喜欢的
3.`batch --out 训练集目录` → 自动整理成 `img_001.jpg...`
4. 把训练集目录交给我,我负责打标 caption 和训练
## 注意事项
- **iPhone 的 HEIC 照片**:工具不支持,先转成 JPG(微信发一遍自己或格式转换工具)
- 照片**别用微信压缩过的**(分辨率不够会被判不合格)
- 想多选就 `--count 25`,想少选就 `--count 15`(推荐 15-25 张)
- 重复判定基于视觉相似度,AI 判断可能有漏网,报告里的人工确认最重要
## 参数速查
```
check <目录> [--out 报告目录]
pick <目录> [--count N] [--out 报告目录]
prepare <目录> --out <训练集目录>
batch <目录> --out <训练集目录>
crop <目录> --out <裁切目录> [--scale 1.8] ← 合影处理:按最大人脸裁切单人
```
## 合影与背景处理(老莫须知)
- **合影**:用 `crop` 命令或 GUI 前先裁切——按最大人脸裁出单人(1.8 倍放大)。裁完短边 <1024 的放弃
- **背景**:不要处理!保持自然多样(室内/户外/墙/街景),caption 写清场景即可。**不要抠图换纯色背景**(会导致 LoRA 把纯色背景和脸绑定,出图全是纯背景)
- **禁止**:磨皮美颜、加水印边框文字
- face_checker 的 prepare/整理会自动处理 EXIF 方向 + RGB 转换
+42
View File
@@ -0,0 +1,42 @@
@echo off
title Face LoRA Console
set "PATH=%SystemRoot%\System32;%SystemRoot%;%PATH%"
set "PY=M:\AI\sd\musubi-tuner\.venv\Scripts\python.exe"
set "GUI=%~dp0tools\caption_gui.py"
set "URL=http://127.0.0.1:7860"
echo ============================================
echo Face LoRA Console (fusion/material/label/train)
echo ============================================
netstat -ano | findstr ":7860" | findstr "LISTENING" >nul 2>&1
if %errorlevel%==0 (
echo [INFO] Console already running. Opening browser...
start "" %URL%
exit /b 0
)
echo [START] Launching console (first start ~1-2 min)...
echo [LOG] Look for the "Face LoRA Console" window on screen (keep it open, closing it stops GUI)
start "Face LoRA Console" "%PY%" "%GUI%"
set /a tries=0
:wait_loop
netstat -ano | findstr ":7860" | findstr "LISTENING" >nul 2>&1
if %errorlevel%==0 goto ready
set /a tries+=1
if %tries% geq 150 goto timeout
timeout /t 1 /nobreak >nul
goto wait_loop
:ready
echo [OK] Console ready. Opening browser...
echo [HINT] The "Face LoRA Console" window = live terminal (real-time logs)
start "" %URL%
exit /b 0
:timeout
echo [WARN] Startup timeout (150s). Check GPU/RAM or port 7860.
pause
exit /b 1
+43
View File
@@ -0,0 +1,43 @@
@echo off
setlocal
cd /d "%~dp0"
echo ============================================
echo Face LoRA Training Launcher (overnight)
echo ============================================
echo.
REM ---- Pre-flight 1: ComfyUI must be CLOSED (holds ~49GB commit + VRAM)
netstat -ano | findstr ":8188" | findstr "LISTENING" >nul 2>&1
if not errorlevel 1 (
echo [ABORT] ComfyUI is still running ^(port 8188 LISTENING^).
echo Training needs the memory and VRAM ComfyUI holds.
echo Please close ComfyUI first, then run this script again.
echo.
pause
exit /b 1
)
echo [OK] ComfyUI not running.
REM ---- Pre-flight 2: need 30GB+ free commit memory
powershell -NoProfile -Command "if ((Get-CimInstance Win32_OperatingSystem).FreeVirtualMemory / 1MB -lt 30) { exit 1 }"
if errorlevel 1 (
echo [ABORT] Less than 30GB free commit memory.
echo Close big apps ^(browser / Photoshop^) and retry.
echo.
pause
exit /b 1
)
echo [OK] Memory check passed.
REM ---- Launch training (only one .py in config dir: the training entry)
cd /d "%~dp0config"
for %%f in (*.py) do (
echo [RUN] %%f
"M:\AI\sd\musubi-tuner\.venv\Scripts\python.exe" "%%f"
goto :after
)
:after
echo.
echo Training process exited. Press any key to close.
pause
+111
View File
@@ -0,0 +1,111 @@
# 素材准备指南(给老莫)
> 训练质量 70% 取决于素材。这步值得认真做,一次做好,训练一次到位。
## 一、选片标准(15-25 张)
**总原则:宁缺毋滥,越对越好,不是越多越好。**
| 要求 | 说明 |
|---|---|
| 数量 | **15-25 张**。少于 10 张易过拟合(只记得训练照),多于 30 张收益递减 |
| 分辨率 | 每张 ≥1024×1024。**用手机原图,不要用微信/朋友圈压缩过的图** |
| 正脸为主 | 正脸/微侧特写占 8 张左右(决定五官比例、皮肤质感) |
| 半身/全身 | 8 张左右(决定身材、服装适配度) |
| 侧面/动态/生活 | 4 张左右(决定泛化能力,换姿势也像) |
| 光线 | **混合光源**(室内灯、自然光、户外),不要全同一个光线 |
| 表情 | 别全是大笑/全是抿嘴,自然表情为主,1-2 张微笑 |
| 遮挡 | **不要**戴口罩、墨镜、厚刘海、帽子遮挡五官 |
| 背景 | 尽量简单(纯色墙、普通室内),避免复杂背景干扰 |
| 年龄 | 照片年龄差距别太大(不要 10 年前的混 3 年前的) |
**反面教材**:20 张全是同一角度的自拍 → LoRA 只会画那个角度,换个姿势就不像了。
**选片来源建议**:翻手机相册挑最近 1-2 年的,证件照、日常照、户外照各来几张。
## 二、打标(每张图配同名 txt)
**原则:描述场景和动作,不描述脸。脸交给 LoRA 记。**
- 每张照片旁边放一个**同名 txt 文件**(如 `img_001.jpg``img_001.txt`
- 内容:触发词 + 一句场景/姿势/服装描述(英文,ComfyUI 生态惯例)
- **禁止写**:五官特征("帅""眼睛大")、具体衣服颜色(会把衣服焊你身上)
- **懒人方案**:把照片丢给 AI(豆包/DeepSeek)批量生成描述,5 分钟搞定
### 触发词(重要)
- 自己取一个**生僻词**,不要在模型词汇里常见
- 建议:`lm_face_v1``sks_laomo` 这类(lm = 老莫)
- 推理时提示词必须带触发词 LoRA 才生效
### 打标示例
```
# img_001.txt(正脸特写)
lm_face_v1, photorealistic portrait, front view, neutral expression, soft indoor lighting, plain background
# img_002.txt(户外全身)
lm_face_v1, full body shot, standing in a park, natural daylight, casual clothing, sharp focus
# img_003.txt(侧脸生活照)
lm_face_v1, three-quarter view, sitting at a cafe table, warm ambient light, candid moment
```
## 三、关键决策规则(老莫必读)
### 1. 年龄:不要混!以最近 1-2 年为主
- 混入多年前的照片 → LoRA 学"平均脸",出图年龄飘忽,两边都不像
- 年龄跨度控制在 **±5 年内**;想要"年轻版"效果用提示词(`younger version`),或以后单独训一个年轻版 LoRA
### 2. 发型:要混!但打标必须写明
- 不同发型(寸头/长发/背头/卷发)都选 → LoRA 泛化强,换发型也像
- **关键**:每张 caption 必须写发型(如 `short black hair`),否则发型会被"焊死"在脸上,出图换发型就不像
### 3. 生活照 vs 艺术照:混,8:2 为主
- **生活照为主**(提供真实身份特征),**艺术照少量**(3-4 张,提供精致造型参考)
- 艺术照**避开**:重度磨皮/美颜(塑料脸)、浓妆(掩盖五官)、棚拍强光死白
- 选轻度修饰、五官清晰的艺术照
### 4. 20 张黄金配方
| 类型 | 张数 |
|---|---|
| 正脸特写 | 8 |
| 半身照 | 6 |
| 全身照 | 4 |
| 侧脸/生活照 | 2 |
### 5. 角度配比(老莫补充 2026-08-03
| 角度 | 张数(20张) | 作用 |
|---|---|---|
| 正脸/微侧(±15°) | 12 | 身份核心 |
| **45° 半侧脸**(左右各一半) | 6 | 立体结构、泛化关键 |
| 纯侧面(90°) | 0-2 | 点缀 |
- 45° 半侧脸教 LoRA 认识脸的立体结构(颧骨/下颌线/鼻子的三维关系),出图换角度也像
- **左右两个方向的 45° 都要有**,别只拍一边
- 纯 90° 侧面信息少,最多 1-2 张
- **俯拍/仰拍:少量或不要**(会扭曲五官比例:俯拍额头大、仰拍鼻孔放大;AI 出图极少用极端俯仰角)。真要放:每方向最多 1 张、15-20° 轻度即可
### 6. 排除清单(看到就换)
口罩 / 墨镜 / 厚刘海遮挡 / 模糊 / 纯侧面 / 背光死黑 / 过度美颜 / 表情全是一个 / 年龄差距大
## 四、交付方式
1. 照片放进 `projects\脸部LoRA训练-Qwen-Image\素材\`(或直接发我,我来整理)
2. 我负责:重命名(img_001...)、检查分辨率/质量、批量生成 caption、整理成训练数据集格式
3. 你只需要:**选 15-25 张照片**
## 四、时间预估
- 选片:10-15 分钟
- 打标:AI 批量 5 分钟(我代劳)
- 合计:老莫只需花 10 分钟选照片
## 五、质量自检清单
- [ ] 15-25 张,全部 ≥1024×1024
- [ ] 正脸/微侧为主,角度有变化
- [ ] 光线有变化,背景不复杂
- [ ] 无遮挡五官(口罩/墨镜/厚刘海)
- [ ] 照片都是最近 1-2 年的
- [ ] 无重复角度、无模糊照
+44
View File
@@ -0,0 +1,44 @@
@echo off
setlocal
set SRC=K:\AI\training\ldf\singled\2. autohandling_test\output\checkpoints
set DST=M:\AI\sd\novelai-webui-aki-v3-r\models\Lora\qwen
echo ============================================
echo Deploy LoRA checkpoints to WebUI
echo ============================================
echo Source: %SRC%
echo Target: %DST%
echo.
if not exist "%SRC%" (
echo [ABORT] Checkpoint dir not found. Run training first.
pause
exit /b 1
)
set COUNT=0
for %%f in ("%SRC%\myface_lora*.safetensors") do (
copy /y "%%f" "%DST%\" >nul
echo [COPIED] %%~nxf
set /a COUNT+=1
)
if %COUNT%==0 (
echo [ABORT] No myface_lora*.safetensors in source dir.
pause
exit /b 1
)
echo.
echo Done. %COUNT% checkpoint(s) deployed to WebUI.
echo.
echo === Verification steps in WebUI ===
echo 1. Select checkpoint: Qwen-Rapid-AIO-NSFW-v23
echo 2. Add LoRA tag to prompt: ^<lora:myface_lora-0000XX:0.8^>
echo (XX = epoch number, e.g. 000040 = epoch 40)
echo 3. Prompt MUST include trigger word: lm_face_v1
echo 4. Compare auto sample images in %SRC%\sample
echo to pick the best epoch first.
echo.
pause
+133
View File
@@ -0,0 +1,133 @@
# 脸部 LoRA 训练项目 · 项目管理总览
> 最后更新:2026-08-09
> 状态:**v3 训练进行中**4090,约 2.5h 完成)
> 唯一入口:本文件 + `docs/云端训练完整操作文档.md`
---
## 项目目标
用老婆的照片训练脸部 LoRA,叠加 `Qwen-Rapid-AIO-NSFW-v23` checkpoint 出图。
训练底模:Qwen-Image-Edit-2511 bf16 官方底模。
---
## 当前训练状态(实时)
| 项 | 值 |
|---|---|
| GPU | RTX 4090$0.74/hv2 验证过的最优解) |
| pod | `k1nzrqh8lfj7jg`IP 103.196.86.68:50155 |
| 训练 | dim32/lr1e-4/120ep/32张=3840步,~2.5s/步,ETA ~2.5h |
| 监控 | 计划任务 `FaceLoRA-Monitor-v3` 每 5 分钟 |
| 进度 | **查 `temp/train_env.json` + `C:\Users\hmo\AppData\Local\Temp\opencode\face_lora\`** |
| 预算 | 4h 封顶($0.74×4=$2.96),余额 $10 |
**如何查当前进度(给 AI / 用户的快速指令)**
```powershell
# 1. 训练实时进度(SSH pod
ssh -i ~/.ssh/id_rsa -p 50155 root@103.196.86.68 "tail -c 500 /workspace/train.log | tr '\r' '\n' | tail -2"
# 2. 监控状态(下载了多少 checkpoint)
Get-Content C:\Users\hmo\AppData\Local\Temp\opencode\face_lora\train_events.log
Get-ChildItem C:\Users\hmo\AppData\Local\Temp\opencode\face_lora\checkpoints_v3\
# 3. pod 是否还在 / 计费
```
---
## 目录结构
```
脸部LoRA训练-Qwen-Image/
├── docs/
│ └── 云端训练完整操作文档.md ← 训练全流程唯一权威参考(含全部踩坑)
├── tools/
│ ├── caption_gui.py ← 打标 GUIhttp://127.0.0.1:7860
│ ├── face_checker.py ← 素材审核/选图/auto流水线
│ ├── monitor_train_v3.ps1 ← 训练监控(计划任务,全自动)★正式版
│ ├── merge_fixed.py ← ComfyUI 融合工具
│ └── validate_lora.py ← 训练后验证
├── cloud/
│ ├── dataset.zip ← 训练集打包(32 img+32 txt
│ ├── bootstrap.sh ← 云端引导(装musubi+下模型+预缓存)
│ ├── train.sh ← 云端训练(含崩溃重启)
│ ├── correct_key.pub ← 正确 SSH key(指纹 7Eep5Qz
│ └── sample_prompts.txt
├── config/
│ ├── train_config.json ← GUI 训练配置(train_dataset/output_dir
│ ├── dataset.toml / *_active ← 数据集配置
│ └── 训练脚本.py ← 本地训练脚本(已废弃,走云端)
├── temp/ ← 运行数据(非正式,可清理)
│ ├── train_env.json ← 监控配置(pod_id/dl_url/ssh/预算)★关键
│ └── ...
├── photos/ 素材/ 打标/ output/ ← 本地素材与输出
└── README.md ← 旧版项目说明
```
---
## 关键文件索引
| 文件 | 用途 | 谁改 |
|---|---|---|
| `docs/云端训练完整操作文档.md` | 训练全流程+踩坑记录 | 训练相关改动必须同步 |
| `tools/monitor_train_v3.ps1` | 训练监控(正式版) | 监控逻辑改动 |
| `temp/train_env.json` | 当前训练 pod 配置 | 每次新 pod 更新 |
| `cloud/train.sh` | 云端训练参数 | 换 GPU/参数时 |
| `cloud/bootstrap.sh` | 云端环境搭建 | 环境变化时 |
---
## 流程速览
1. **素材准备**`photos_原始/``face_checker.py auto` → GUI ②候选换图筛选
2. **整理训练集**GUI「📦 整理训练集」→ `train_dataset/`(自动跟随素材目录)
3. **打包**`cloud/dataset.zip`(含 img+txt
4. **云端训练**:创建 pod → 修 SSH key → 上传 → bootstrap → train(详见 docs
5. **监控下载**:计划任务自动增量下载 checkpoint 到 `checkpoints_v3/`
6. **验证**checkpoint → WebUI Rapid-AIO 出图
---
## ⚠️ 血泪教训(必须遵守,详见 docs 第八章)
1. **只用 Secure Cloud**,禁 Community
2. **换 GPU 前查架构兼容**5090(sm_120) 与镜像 PyTorch 不兼容,禁用;4090/A40 可用
3. **预缓存验证看 `_qie.safetensors`**(数量=素材数),不是 .npz
4. **PS 5.1 变量名不区分大小写**:路径变量别叫 `$STATE`(会被 `$state` 覆盖),用 `$STATEFILE`
5. **PS 5.1 脚本存 GBK 编码**,输出目录用纯 ASCII,状态用纯文本(.json 写入不可靠)
6. **train_env.json 读取失败必须退出**,绝不用空值删 pod
7. **监控全自动**:计划任务每 5 分钟,增量下载 checkpoint,超时/崩溃自动止损,完成自动删 pod
8. **文档保持更新**:改了就要同步 docs/(不用问用户)
---
## ⚠️ ComfyUI workflow 编写要点(2026-08-09 查证,别手写 link id!)
**血泪教训**:直接改 workflow JSON 加连接(手写 link id 44-47)→ 加载显示正确但实际不稳定(操作其他连线时连接被替换)。用户前端重连(link id 47-50)后稳定。
**根因(2026-08-09 源码级验证,LiteGraph + ComfyUI 前端 TS 版)**
- workflow UI format 的 link 是 6 元组 `[id, src_node, src_slot, dst_node, dst_slot, type]`
- **连接 = 三方引用必须一致**:① links 数组 ② 目标 node 的 `inputs[].link` ③ 源 node 的 `outputs[].links`(数组,一个输出可扇出多条)
- 只改 links 数组不动 node 引用 → 线"幽灵"或消失
- **核心机制**:连接新线时 `linkId = lastLinkId + 1`;前端加载 workflow 时 `state.lastLinkId = Math.max(当前, JSON值)`,**不扫描 links 数组**。脚本手写 link id 但没同步 lastLinkId → 下次连接新线生成重复 id → `_links.set(id, 新线)` 覆盖手写的连接
- **铁律:脚本加 link 后必须把 `state.lastLinkId`(新schema)或 `last_link_id`(旧schema)设为 ≥ 最大 link id**
- **正确做法**:连接让前端生成,或脚本严格 lastLinkId 同步 + 三方一致
**关键规则**
1. API format(执行):`{"id": {"class_type":..., "inputs":{...}}}`,连接 `["源ID字符串", 输出槽]`**节点 ID 必须字符串**
2. UI format(画布):顶层 nodes+links+groups**保存画布用 UI format**API→UI 有损丢布局)
3. 写节点前查 `/object_info/<NodeType>` 拿真实 schema,别猜 widgets 顺序
4. 类型不匹配插转换节点(IMAGE↔LATENT 用 VAEEncode/VAEDecode
5. 验证阶梯:静态检查 → /object_info → 小图 /prompt 冒烟 → 全量
6. 参考 skill`SlavaSexton/ComfyUI-Agent-Kit`(最全)+ `artokun/comfyui-mcp`(精简权威)
---
## 权限与密钥(不提交 Git
- RunPod API key`temp/../.runpod_api_key`rpa_ 开头,50位)
- SSH key`~/.ssh/id_rsa`(指纹 7Eep5Qz,对应 cloud/correct_key.pub
- Giteaxiaoxiao 凭证(curl 拉 correct_key.pub 用)
- GUI 状态:`tools/gui_state.json`