docs: translate v2.5.5 technical documentation to English

This commit is contained in:
ussoewwin
2026-08-12 19:01:54 +09:00
parent 6791edf402
commit aaec404cf9
+151 -145
View File
@@ -1,58 +1,60 @@
# v2.5.5 技術解説書: Krea2 OpenPose ControlNet LoRA サポート
# v2.5.5 Technical Documentation: Krea2 OpenPose ControlNet LoRA Support
> 起点コミット: `75f99be1614b18e22c5d58a7c24115d5218e488e`(v2.5.4 時点)
> 対象コミット: `b7cbf11` / `042fe16` / `07ef7ec`(コード)+ `999bac6` / `533495f` / `67f84e6` / `5797d06`(ドキュメント)
> Baseline commit: `75f99be1614b18e22c5d58a7c24115d5218e488e` (v2.5.4)
> Code commits: `b7cbf11` / `042fe16` / `07ef7ec` — Docs commits: `999bac6` / `533495f` / `67f84e6` / `5797d06`
---
## ① 目的
## ① Purpose
Krea2(`SingleStreamDiT`)向け ControlNet LoRA のうち、**openpose 版**
(`krea2_turbo_openpose_controlnet.safetensors`)のサポートを追加する。
Add support for the **openpose** variant of the Krea2 (`SingleStreamDiT`) ControlNet LoRA
(`krea2_turbo_openpose_controlnet.safetensors`).
### 背景
### Background
- v2.5.1 で Krea2 **depth** ControlNet LoRA(`krea2-depth-control-lora.safetensors`)を実装済み。
これは「拡張 first 投影(`first.weight` を 2 倍入力チャンネルに拡張)+ コントロール潜像注入」方式。
- openpose 版は構造が根本的に異なる:
- `first.weight` 拡張**なし**(228MB / bfloat16 / rank 32)
- `diffusion_model.blocks.{0-27}` の 28 DiT ブロック + `diffusion_model.txtfusion.layerwise_blocks.{0,1}` /
`refiner_blocks.{0,1}` の**純 LoRA**(256 ペア)
- 訓練時の条件経路が **Kontext 式 reference latent**(`index_timestep_zero`)であり、
ostris の `comfyui-krea2-ostris-edit` や pysssss の `krea2_controlnet_pose.json` ワークフローが
kv_cache=true で参照画像を注入する方式で訓練されている
- v2.5.1 implemented Krea2 **depth** ControlNet LoRA (`krea2-depth-control-lora.safetensors`)
using the "expanded first projection" approach (`first.weight` expanded to 2x input channels
+ control-latent injection).
- The openpose variant is structurally different:
- **No** `first.weight` expansion (228MB / bfloat16 / rank 32)
- Pure LoRA over `diffusion_model.blocks.{0-27}` (28 DiT blocks) +
`diffusion_model.txtfusion.layerwise_blocks.{0,1}` / `refiner_blocks.{0,1}` (256 pairs total)
- The conditioning pathway used at training time is the **Kontext-style reference latent**
(`index_timestep_zero`), i.e. the reference-image injection convention used by ostris'
`comfyui-krea2-ostris-edit` and pysssss' `krea2_controlnet_pose.json` workflow (kv_cache=true).
### 要求事項
### Requirements
1. **排他的分岐**: openpose は depth / zimage / nunchaku_qwenimage / qwenimage_standard に
一切影響させない。既存 4 ルートのコードは無変更(インデント調整のみ)であること。
2. **自己完結**: ostris / pysssss のノードやワークフローに依存せず、このリポジトリ単体で
同じ pose 制御効果を実現する。
3. **ローダー非変更**: `Krea2ControlNetLoraLoader` と `__init__.py` は変更しない
(state_dict を MODEL_PATCH で運ぶだけ、分岐判定は `controlnet.py` 側で完結)。
1. **Exclusive routing**: openpose must not affect depth / zimage / nunchaku_qwenimage /
qwenimage_standard in any way. The existing 4 routes must remain unchanged (indentation only).
2. **Self-contained**: reproduce the same pose-control effect within this repository alone,
without depending on ostris / pysssss nodes or workflows.
3. **Loader untouched**: `Krea2ControlNetLoraLoader` and `__init__.py` are not modified
(the loader just carries the state_dict via MODEL_PATCH; all routing decisions live in
`controlnet.py`).
---
## ② 新規作成・修正したファイル名
## ② New / Modified Files
| ファイル | 種別 | 変更内容 |
| File | Type | Change |
|---|---|---|
| `nodes/controlnet.py` | 修正(+382 行) | openpose ルート実装の中核。新規関数 13 個 + 分岐・INPUT_TYPES 変更 |
| `README.md` | 修正(+60) | Krea2 Control (Depth & OpenPose) セクション追加 |
| `zhmd/README.md` | 修正(+569/-530 実質) | 中文版 README 同内容 |
| `RELEASE_NOTES/changelog.md` | 修正(+10) | v2.5.5 エントリ追加 |
| `zhmd/changelogzh.md` | 修正(+9) | 中文版 changelog 同内容 |
| `RELEASE_NOTES/v2.5.5.md` | **新規** | 本技術解説書 |
| `nodes/controlnet.py` | modified (+382 lines) | Core of the openpose route. 13 new functions + routing / INPUT_TYPES changes |
| `README.md` | modified (+60) | New "Krea2 Control (Depth & OpenPose)" section |
| `zhmd/README.md` | modified | Chinese README, same content |
| `RELEASE_NOTES/changelog.md` | modified (+10) | v2.5.5 entry |
| `zhmd/changelogzh.md` | modified (+9) | Chinese changelog, same content |
| `RELEASE_NOTES/v2.5.5.md` | **new** | This technical documentation |
`__init__.py` は**無変更**(要件 3 を満たす)。
`__init__.py` is **unchanged** (requirement 3).
---
## ③ 新規作成・修正したコード全文
## ③ Full New / Modified Code
### 3.1 `nodes/controlnet.py` の変更
### 3.1 `nodes/controlnet.py` changes
#### (a) import 追加
#### (a) Added imports
```python
import math
@@ -77,7 +79,7 @@ import logging
logger = logging.getLogger(__name__)
```
#### (b) `_krea2_target_key` に txtfusion ルーティング追加
#### (b) `_krea2_target_key`: txtfusion routing added
```python
def _krea2_target_key(base):
@@ -91,12 +93,12 @@ def _krea2_target_key(base):
changed = True
if base.startswith("blocks."):
return f"diffusion_model.{base}.weight"
if base.startswith("txtfusion."): # ← 追加
if base.startswith("txtfusion."): # ← added
return f"diffusion_model.{base}.weight"
return None
```
#### (c) 新規関数群(`_classify_krea2_control_type` 〜 `_apply_krea2_openpose_control`)
#### (c) New function group (`_classify_krea2_control_type` .. `_apply_krea2_openpose_control`)
```python
def _classify_krea2_control_type(state_dict):
@@ -445,7 +447,7 @@ def _apply_krea2_openpose_control(model_patched, model_patch, vae, image, streng
logger.info("[Krea2OpenposeControl] kv_cache isolated-ref forward armed")
```
#### (d) INPUT_TYPES 変更(`use_kv_cache` 追加)
#### (d) INPUT_TYPES change (`use_kv_cache` added)
```python
@classmethod
@@ -460,7 +462,7 @@ def _apply_krea2_openpose_control(model_patched, model_patch, vae, image, streng
"use_kv_cache": ("BOOLEAN", {"default": True, "tooltip": "Isolated reference K/V cache mode (ai-toolkit kv_cache training convention). Leave on for Krea2 control LoRAs trained with kv_cache."})}}
```
#### (e) 実行分岐変更(depth / openpose 排他)
#### (e) Routing change (depth / openpose exclusive)
```python
elif route == "krea2":
@@ -485,163 +487,167 @@ def _apply_krea2_openpose_control(model_patched, model_patch, vae, image, streng
)
```
### 3.2 ドキュメント変更(概要)
### 3.2 Documentation changes (summary)
- `README.md` / `zhmd/README.md`: ノード説明の更新 + `### Krea2 Control (Depth & OpenPose)`
セクション新設(`#### Krea2 Depth Control` / `#### Krea2 OpenPose Control`)。
- `RELEASE_NOTES/changelog.md` / `zhmd/changelogzh.md`: v2.5.5 エントリ追加。
- `README.md` / `zhmd/README.md`: node descriptions updated + new
`### Krea2 Control (Depth & OpenPose)` section (`#### Krea2 Depth Control` /
`#### Krea2 OpenPose Control`).
- `RELEASE_NOTES/changelog.md` / `zhmd/changelogzh.md`: v2.5.5 entry added.
---
## ④ コードの詳細な解説
## ④ Detailed Code Explanation
### 4.0 全体アーキテクチャ
### 4.0 Overall Architecture
```
Krea2ControlNetLoraLoader (変更なし)
│ MODEL_PATCH(LoRA safetensors の state_dict を内包)
Krea2ControlNetLoraLoader (unchanged)
│ MODEL_PATCH (carries the LoRA safetensors state_dict)
▼
NunchakuQI&ZITDiffsynthControlnet.diffsynth_controlnet_nunchaku
│ _classify_controlnet_target(model, model_patch)
│ → "zimage" | "nunchaku_qwenimage" | "qwenimage_standard" | "krea2" | "unknown"
▼ route == "krea2"
_classify_krea2_control_type(state_dict) ← first.weight の有無
├─ "depth" → _apply_krea2_control() (既存、無変更)
└─ "openpose" → _apply_krea2_openpose_control()(新規)
├─ add_patches(ブロック LoRA 256 キー)
├─ extra_conds パッチ(reference_latents 注入)
└─ diffusion_model.forward パッチ(kv_cache モード)
_classify_krea2_control_type(state_dict) ← presence of first.weight
├─ "depth" → _apply_krea2_control() (existing, unchanged)
└─ "openpose" → _apply_krea2_openpose_control() (new)
├─ add_patches(256 block LoRA keys)
├─ extra_conds patch (reference_latents injection)
└─ diffusion_model.forward patch (kv_cache mode)
```
### 4.1 `_classify_krea2_control_type` — depth / openpose の排他判定
### 4.1 `_classify_krea2_control_type` — depth / openpose exclusive classification
LoRA ファイルの state_dict に `first.weight` 系キーが存在するかで判定する。
Decides by the presence of a `first.weight`-style key in the LoRA state_dict.
- **depth**: `first.weight` 拡張((6144, 128))を持つ → 従来の depth ルート
- **openpose**: `first` キーなし(純ブロック LoRA)→ openpose ルート
- **depth**: has the expanded `first.weight` ((6144, 128)) → existing depth route
- **openpose**: no `first` key (pure block LoRA) → openpose route
depth ファイルと openpose ファイルは実物の構造が明確に異なる(depth: 861MB float32 rank64
+ first 拡張 / openpose: 228MB bf16 rank32 純 LoRA)ため、誤判定の余地がない。
execute 側も `if/elif` なので**両ルートが同時に適用されることは構造的に不可能**。
The two files are structurally distinct (depth: 861MB float32 rank64 + first expansion /
openpose: 228MB bf16 rank32 pure LoRA), so there is no room for misclassification.
The execute side uses `if/elif`, so **both routes can never be applied simultaneously**.
### 4.2 `_krea2_fit_ref_image` — 参照画像の前処理
### 4.2 `_krea2_fit_ref_image` — reference image preprocessing
ai-toolkit の Krea2 reference-latent 規約に合わせる:
Follows the ai-toolkit Krea2 reference-latent convention:
- 最大 **1MP**(1024×1024)に**縮小のみ**(拡大はしない)
- 縦横を **/16 にスナップ**(DiT の patch=2 のトークン化に安全)
- `common_upscale(..., "area", "disabled")` でリサイズ(area = ダウンスケールに適した補間)
- Downscale **only** (never upscale) to at most **1MP** (1024×1024)
- Snap width/height to **/16** (safe for the DiT patch=2 tokenization)
- Resize via `common_upscale(..., "area", "disabled")` (area = suitable for downscaling)
### 4.3 `_krea2_make_ref_cond_patch` — ネイティブ ref 注入(extra_conds パッチ)
### 4.3 `_krea2_make_ref_cond_patch` — native ref injection (extra_conds patch)
Krea2 コアモデル(`comfy/model_base.py` の `Krea2` クラス)は
`extra_conds` が `reference_latents` と `reference_latents_method` を処理する。
The Krea2 core model (`Krea2` class in `comfy/model_base.py`) consumes
`reference_latents` / `reference_latents_method` produced by `extra_conds`.
- `extra_conds()`: 出力 dict に `ref_latents`(`CONDList`)と
`ref_latents_method = "index_timestep_zero"` を追加
- `extra_conds_shapes()`: メモリプランニング用に平坦化形状 `[1, 16, prod//16]` を追加
- **二重注入防止**: `kwargs["reference_latents"]` が既にある(= `TextEncodeKrea2OstrisEdit`
併用時)か `out` に `ref_latents` が既にある場合は**スキップ**。
→ モデルノード単体でも動き、VLM テキストエンコーダ併用時も安全。
- `extra_conds()`: adds `ref_latents` (`CONDList`) and
`ref_latents_method = "index_timestep_zero"` to the output dict
- `extra_conds_shapes()`: adds the flattened shape `[1, 16, prod//16]` for memory planning
- **Double-injection guard**: skipped when `kwargs["reference_latents"]` already exists
(= when `TextEncodeKrea2OstrisEdit` is used in parallel) or `ref_latents` is already in `out`.
→ Works standalone, and is safe to combine with a VLM text encoder.
### 4.4 `_krea2_ref_pack` / `_krea2_patchify` — ref トークン化と RoPE 座標
### 4.4 `_krea2_ref_pack` / `_krea2_patchify` — ref tokenization and RoPE positions
`_krea2_patchify` は通常の画像トークン化 `(B,C,H,W) → (B, L, C·p²)`。
`_krea2_patchify` is the regular image tokenization `(B,C,H,W) → (B, L, C·p²)`.
`_krea2_ref_pack` は参照潜像をトークン化し、**軸 0 のインデックスを i+1** にする
(text=0、ref1=1、ref2=2、… の ai-toolkit 規約):
`_krea2_ref_pack` tokenizes reference latents and sets the **axis-0 index to i+1**
(the ai-toolkit convention: text=0, ref1=1, ref2=2, ...):
```
rid[..., 0] = i ← 参照インデックス(axis-0)
rid[..., 1] = y グリッド
rid[..., 2] = x グリッド
rid[..., 0] = i ← reference index (axis-0)
rid[..., 1] = y grid
rid[..., 2] = x grid
```
- 5D 潜像 `(B, C, T, H, W)`(Wan21 VAE、T=1)は 4D に reshape
- `pad_to_patch_size` + `repeat_to_batch_size` でバッチ整合
- 5D latents `(B, C, T, H, W)` (Wan21 VAE, T=1) are reshaped to 4D
- `pad_to_patch_size` + `repeat_to_batch_size` handle padding and batch alignment
### 4.5 `_krea2_ref_attn_kv` — Attention の K/V capture / 注入
### 4.5 `_krea2_ref_attn_kv` — Attention with K/V capture / injection
Krea2 の `Attention` を再実装し、2 つの拡張ポイントを持つ:
Re-implements Krea2 `Attention` with two extension points:
- **capture モード**(`kv_capture`): RoPE 適用後・GQA 展開前の `(k, v)` を記録
- **cache モード**(`kv_cache`): 計算済み ref K/V を `torch.cat` で**キー側に追加**
- **capture mode** (`kv_capture`): records `(k, v)` after RoPE, before GQA expansion
- **cache mode** (`kv_cache`): appends the precomputed ref K/V to the key side via `torch.cat`
処理順: `wq/wk/wv/gate` → reshape → `qknorm` → `apply_rope` → capture/cache →
GQA 展開(`repeat_interleave`)→ `optimized_attention_masked` → `wo(out·sigmoid(gate))`。
Order: `wq/wk/wv/gate` → reshape → `qknorm` → `apply_rope` → capture/cache →
GQA expansion (`repeat_interleave`) → `optimized_attention_masked` → `wo(out·sigmoid(gate))`.
### 4.6 `_krea2_ref_precompute_kv` — t=0 参照専用パス
### 4.6 `_krea2_ref_precompute_kv` — t=0 reference-only pass
**kv_cache モードの核**。参照トークンだけを全ブロックに通し、各ブロックの
post-RoPE K/V を記録する:
The **core of kv_cache mode**. Runs only the reference tokens through all blocks and
records each block's post-RoPE K/V:
1. `_krea2_ref_pack` で `(reftok, refpos)` を生成
2. `dit.first(reftok)` で埋め込み
3. **t=0**(`timestep_embedding(zeros)`)のタイムベクトルで条件付け
4. 28 ブロックを `kv_capture` 付きで前進し、`ref_kv` リスト(28 個の (k, v))を得る
1. `_krea2_ref_pack` produces `(reftok, refpos)`
2. `dit.first(reftok)` embeds the tokens
3. Conditioned on the **t=0** time vector (`timestep_embedding(zeros)`)
4. Runs the 28 blocks with `kv_capture`, collecting `ref_kv` (28 (k, v) pairs)
参照トークンはテキストやノイズ画像を見ないため、**1 回のパスが全デノイズステップに再利用**できる。
Reference tokens never see text or noisy images, so **one pass serves the whole denoise**.
### 4.7 `_krea2_forward_with_cached_refs` — デノイズ本体の forward
### 4.7 `_krea2_forward_with_cached_refs` — the denoising forward
通常の Krea2 forward の再実装。ライブシーケンスは「テキスト + ノイズ画像」のみで、
各ブロックの attention にキャッシュ済み ref K/V を extra key として追加する:
A re-implementation of the regular Krea2 forward. The live sequence is only
"text + noisy image tokens"; each block's attention appends the cached ref K/V
as extra keys:
- 5D 入力(動画・temporal)は 4D に flatten、出力で復元
- `_unpack_context` → `txtfusion` → `txtmlp` でテキストを処理
- text 位置は axis-0=0、画像位置は y/x グリッドで RoPE 座標を構成
- 出力は `dit.last` → 画像トークンのみ切り出し → patch 逆変換 → 元サイズ復元
- 5D inputs (temporal) are flattened to 4D and restored on output
- Text processed via `_unpack_context` → `txtfusion` → `txtmlp`
- RoPE positions: text at axis-0=0, image tokens on a y/x grid
- Output: `dit.last` → slice image tokens → patch unpatch → restore original size
### 4.8 `_krea2_ref_fingerprint` — キャッシュの内容キー
### 4.8 `_krea2_ref_fingerprint` — content key for the cache
各ステップで `ref_latents` テンソルは再構築されるためオブジェクト ID は使えない。
形状 + 和 + 二乗和をキーにして内容ベースでキャッシュを識別する。
`ref_latents` tensors are rebuilt every step, so object identity cannot be used.
Shape + sum + sum-of-squares serve as a content-based key.
### 4.9 `_krea2_make_ref_kv_forward` — forward 差し替えの状態管理
### 4.9 `_krea2_make_ref_kv_forward` — forward replacement state management
`diffusion_model.forward` を差し替え、以下のロジックを実装:
Replaces `diffusion_model.forward`:
1. `ref_latents` が無ければ**元の forward にそのまま委譲**(ノード非 openpose 時と同一挙動)
2. **新規実行検出**: sigma は実行内で単調減少する性質を使い、`sig > last_sigma` なら
新しい実行とみなしてキャッシュをクリア(`sample_sigmas[0]` との一致でも検出)
3. キャッシュに無ければ `_krea2_ref_precompute_kv` で K/V を事前計算して保存
4. `_krea2_forward_with_cached_refs` でデノイズ
1. No `ref_latents` → **delegate to the original forward** (identical to non-openpose behavior)
2. **New-run detection**: sigmas decrease monotonically within a run, so
`sig > last_sigma` (or matching `sample_sigmas[0]`) clears the cache
3. On cache miss, `_krea2_ref_precompute_kv` computes and stores the K/V
4. `_krea2_forward_with_cached_refs` runs the denoise
### 4.10 `_apply_krea2_openpose_control` — ノードからのエントリポイント
### 4.10 `_apply_krea2_openpose_control` — entry point from the node
1. **ブロック LoRA**: `_krea2_build_block_patches`(既存関数)で
`diffusion_model.blocks.*` + `diffusion_model.txtfusion.*` の 256 キーを
`LoRAAdapter` として構築し、`add_patches(strength_patch=strength)` で適用
2. **ref 注入**: `_krea2_fit_ref_image` → `vae.encode` → `_krea2_make_ref_cond_patch`
を `extra_conds` / `extra_conds_shapes` のオブジェクトパッチとして登録
3. **kv_cache**: `use_kv_cache=True`(デフォルト)なら `diffusion_model.forward` を
`_krea2_make_ref_kv_forward` で差し替え
4. vae / image 欠落時は LoRA のみ適用して警告ログ(ref 注入なしでもクラッシュしない)
1. **Block LoRA**: `_krea2_build_block_patches` (existing) builds the 256 keys of
`diffusion_model.blocks.*` + `diffusion_model.txtfusion.*` as `LoRAAdapter` and
applies them via `add_patches(strength_patch=strength)`
2. **Ref injection**: `_krea2_fit_ref_image` → `vae.encode` → `_krea2_make_ref_cond_patch`
registered as `extra_conds` / `extra_conds_shapes` object patches
3. **kv_cache**: with `use_kv_cache=True` (default), `diffusion_model.forward` is replaced
by `_krea2_make_ref_kv_forward`
4. Missing vae/image → LoRA-only with a warning log (no crash even without ref injection)
### 4.11 排他性の保証(重要)
### 4.11 Exclusivity Guarantee (important)
| ルート | 変更 | 根拠 |
| Route | Change | Basis |
|---|---|---|
| zimage | 無変更 | `_classify_controlnet_target` の分岐コードは 75f99be から同一 |
| nunchaku_qwenimage | 無変更 | 同上 |
| qwenimage_standard | 無変更 | 同上 |
| krea2 depth | **インデントのみ** | `if/elif` 化に伴う字下げ(`git diff` で確認済み) |
| krea2 openpose | 新規 | 新規関数群 + 分岐追加 |
| zimage | unchanged | `_classify_controlnet_target` branch code identical since 75f99be |
| nunchaku_qwenimage | unchanged | same |
| qwenimage_standard | unchanged | same |
| krea2 depth | **indentation only** | re-indented for the `if/elif` structure (verified via `git diff`) |
| krea2 openpose | new | new functions + routing |
- `use_kv_cache` は **optional** 入力(デフォルト True)なので既存ワークフローは互換。
openpose ルートでのみ参照され、他の 4 ルートには渡らない。
- `_classify_controlnet_target` の判定順は zimage → nunchaku → qwenimage_standard → krea2。
`SingleStreamDiT` は ComfyUI core の `comfy/ldm/krea2/model.py` にのみ存在するため、
他モデルが Krea2 に誤分類されることはない。
- `use_kv_cache` is an **optional** input (default True), so existing workflows stay
compatible. It is only referenced by the openpose route and never reaches the other 4.
- `_classify_controlnet_target` checks zimage → nunchaku → qwenimage_standard → krea2 in order.
`SingleStreamDiT` exists only in `comfy/ldm/krea2/model.py`, so no other model family
can be misclassified as Krea2.
### 4.12 検証結果
### 4.12 Verification Results
- 構文チェック / `python_embeded` での import 成功(INPUT_TYPES・シグネチャ確認)
- ユニットテスト: patchify 形状、ref_pack の axis-0=1・グリッド・5D 処理、fingerprint、
1MP スナップ、排他ルート(zimage / nunchaku / qwenimage_standard / depth)無変更
- **実機 INT8 検証**: HSWQ `moodyKrea2Mix_v50_sci_1off_convrot_int8.safetensors` 上で
`patch_weight_to_device` による bake を確認 → LoRA 差分が weight に反映
(相対変化 7.76%)。HSWQ v7 bake フック経由でも `int8_baked=256, patches_left=0` を確認。
- 実機ログ: `route=krea2` → `sub-route: openpose` → `Applied 256 block LoRA patches` →
- Syntax check / import via `python_embeded` succeeded (INPUT_TYPES and signature verified)
- Unit tests: patchify shapes, ref_pack axis-0=1 grid / 5D handling, fingerprint,
1MP snap, and that the exclusive routes (zimage / nunchaku / qwenimage_standard / depth)
are unchanged
- **Live INT8 verification**: on HSWQ `moodyKrea2Mix_v50_sci_1off_convrot_int8.safetensors`,
`patch_weight_to_device` bake confirmed the LoRA delta reaches the weights
(relative change 7.76%). The HSWQ v7 bake hook also reported
`int8_baked=256, patches_left=0`.
- Live run log: `route=krea2` → `sub-route: openpose` → `Applied 256 block LoRA patches` →
`control ref latent shape=(1, 16, 1, 128, 128)` → `kv_cache isolated-ref forward armed`
→ 12/12 ステップ完走(エラーなし)。
→ 12/12 steps completed (no errors).