summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-09-13 22:02:44 +0900
committertanaka takashi <isazaka@gmail.com>2026-09-13 22:02:44 +0900
commit213e738140859dbcc12c2540ce40d6b6d2875aa7 (patch)
tree93443693ffbfbfb8706e1eea2f857d677d62cb4a
parentde44625e3baae4d43ad096248b7096578b11e0b0 (diff)
ddim timestep_spacingをtrailingに強制するモードを追加
-rw-r--r--doc/Design.md6
-rw-r--r--doc/Specifications.md3
-rw-r--r--doc/config.md24
-rw-r--r--src/mcp_sd15/config.py4
-rw-r--r--src/mcp_sd15/pipeline_factory.py5
-rw-r--r--src/mcp_sd15/service.py1
-rw-r--r--tests/test_config.py11
-rw-r--r--tests/test_pipeline_factory.py1
8 files changed, 52 insertions, 3 deletions
diff --git a/doc/Design.md b/doc/Design.md
index b5e433b..9224158 100644
--- a/doc/Design.md
+++ b/doc/Design.md
@@ -197,11 +197,17 @@ Schedulerの文字列変換は`PipelineFactory`へ集約し、Tool層とServiceå
| `dpmpp_2m_sde_karras` | `DPMSolverMultistepScheduler` | `algorithm_type="sde-dpmsolver++"`, `solver_order=2`, `use_karras_sigmas=True` |
| `euler_a` | `EulerAncestralDiscreteScheduler` | pipeline scheduler configを継承 |
| `ddim` | `DDIMScheduler` | pipeline scheduler configを継承 |
+| `ddim_trailing` | `DDIMScheduler` | `timestep_spacing="trailing"` |
| `lcm` | `LCMScheduler` | pipeline scheduler configを継承 |
LCM-LoRAも通常のLoRAと同じ`[[model.lora]]`でロードする。`lcm`選択時にstepsやCFGを
自動調整せず、TOMLの`generation.steps`と`generation.cfg_scale`をそのままpipelineへ渡す。
+`ddim_trailing`は`ddim`と同じ`DDIMScheduler`を使用し、`timestep_spacing="trailing"`だけを
+明示的に上書きする。Hyper-SD等の固定step LoRAと組み合わせることを想定しているが、
+LoRAファイル名による自動判定やsteps・CFGの自動補正は行わない。生成結果の再現性を保つため、
+実際に使用したscheduler名は生成メタデータへそのまま記録する。
+
---
## 9. ControlNet設計
diff --git a/doc/Specifications.md b/doc/Specifications.md
index cc178bb..c038abf 100644
--- a/doc/Specifications.md
+++ b/doc/Specifications.md
@@ -198,7 +198,7 @@ negative_file = "negative.txt"
### 7.1 Scheduler
-`generation.scheduler`は次の5種類のみを許可し、既定値は`dpmpp_2m_karras`とする。
+`generation.scheduler`は次の6種類のみを許可し、既定値は`dpmpp_2m_karras`とする。
| 値 | 動作 |
|---|---|
@@ -206,6 +206,7 @@ negative_file = "negative.txt"
| `dpmpp_2m_sde_karras` | DPM++ 2M SDE + Karras sigmas |
| `euler_a` | Euler Ancestral |
| `ddim` | DDIM |
+| `ddim_trailing` | DDIM。`timestep_spacing="trailing"`を強制 |
| `lcm` | Latent Consistency Model scheduler |
`lcm`選択時も`steps`と`cfg_scale`は設定値をそのまま使用する。LoRAファイル名から
diff --git a/doc/config.md b/doc/config.md
index 69dd3d6..5d7db2a 100644
--- a/doc/config.md
+++ b/doc/config.md
@@ -162,12 +162,15 @@ pad_last_block = false
| `dpmpp_2m_karras` | `DPMSolverMultistepScheduler`。DPM++ 2M、Karras sigmas |
| `dpmpp_2m_sde_karras` | `DPMSolverMultistepScheduler`。DPM++ 2M SDE、Karras sigmas |
| `ddim` | DDIM |
+| `ddim_trailing` | DDIM。`timestep_spacing="trailing"`を強制 |
| `euler_a` | Euler Ancestral Discrete |
| `lcm` | Latent Consistency Model scheduler |
`dpmpp_2m_karras`は`algorithm_type="dpmsolver++"`、`solver_order=2`、
`use_karras_sigmas=true`で構築する。`dpmpp_2m_sde_karras`は`algorithm_type`だけを
-`sde-dpmsolver++`へ変更する。他のschedulerはpipeline scheduler configを引き継ぐ。
+`sde-dpmsolver++`へ変更する。`ddim_trailing`は`ddim`と同じ`DDIMScheduler`を使用し、
+`timestep_spacing="trailing"`だけを明示的に上書きする。他のschedulerはpipeline scheduler
+configを引き継ぐ。
### LCM-LoRA設定例
@@ -188,6 +191,25 @@ default_size = "square_512"
`lcm`選択時も`steps`と`cfg_scale`はTOML値をそのまま使用する。LCM-LoRAのファイル名を
自動判定して設定を書き換えることはない。
+### Hyper-SD設定例
+
+Hyper-SD15固定step LoRAも通常のLoRAと同じ方法で指定する。`ddim_trailing`は
+Hyper-SD向けの推奨schedulerであり、`timestep_spacing="trailing"`を強制する。
+stepsや`cfg_scale`はTOML値をそのまま使用し、LoRAファイル名による自動判定や自動補正は
+行わない。
+
+```toml
+[[model.lora]]
+file = "Hyper-SD15-4steps-lora.safetensors"
+weight = 1.0
+
+[generation]
+steps = 4
+cfg_scale = 0.0
+scheduler = "ddim_trailing"
+default_size = "square_512"
+```
+
### `default_size`
MCPツールで`size`を省略した場合に使用するサイズIDである。
diff --git a/src/mcp_sd15/config.py b/src/mcp_sd15/config.py
index 39f31c9..c82cb6e 100644
--- a/src/mcp_sd15/config.py
+++ b/src/mcp_sd15/config.py
@@ -11,7 +11,9 @@ from typing import Any
from .errors import ConfigError
VALID_SIZES = frozenset({"square_512", "landscape_512", "portrait_512"})
-VALID_SCHEDULERS = frozenset({"dpmpp_2m_karras", "dpmpp_2m_sde_karras", "euler_a", "ddim", "lcm"})
+VALID_SCHEDULERS = frozenset(
+ {"dpmpp_2m_karras", "dpmpp_2m_sde_karras", "euler_a", "ddim", "ddim_trailing", "lcm"}
+)
VALID_TRANSPORTS = frozenset({"stdio", "streamable-http"})
diff --git a/src/mcp_sd15/pipeline_factory.py b/src/mcp_sd15/pipeline_factory.py
index 02fe882..e865555 100644
--- a/src/mcp_sd15/pipeline_factory.py
+++ b/src/mcp_sd15/pipeline_factory.py
@@ -151,6 +151,11 @@ class PipelineFactory:
solver_order=2,
use_karras_sigmas=True,
)
+ if name == "ddim_trailing":
+ return diffusers.DDIMScheduler.from_config(
+ current.config,
+ timestep_spacing="trailing",
+ )
classes = {
"euler_a": diffusers.EulerAncestralDiscreteScheduler,
"ddim": diffusers.DDIMScheduler,
diff --git a/src/mcp_sd15/service.py b/src/mcp_sd15/service.py
index 65b62e7..c1feade 100644
--- a/src/mcp_sd15/service.py
+++ b/src/mcp_sd15/service.py
@@ -96,6 +96,7 @@ class ImageGenerationService:
"seed": seed,
"size": str(request.size),
"mode": method,
+ "scheduler": self.config.generation.scheduler,
"timestamp": datetime.now(UTC).isoformat(),
"elapsed_seconds": round(time.monotonic() - started, 3),
}
diff --git a/tests/test_config.py b/tests/test_config.py
index 5d339f8..0e34086 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -282,6 +282,17 @@ def test_generation_defaults_clip_skip_and_pad_last_block(tmp_path: Path) -> Non
assert config.generation.pad_last_block is False
+def test_generation_accepts_ddim_trailing_scheduler(tmp_path: Path) -> None:
+ path = tmp_path / "config.toml"
+ path.write_text(
+ f'[model]\nroot = "{tmp_path.as_posix()}"\ncheckpoint = "base.safetensors"\n'
+ '[generation]\nscheduler = "ddim_trailing"\n',
+ encoding="utf-8",
+ )
+ config = load_config(path, validate_paths=False)
+ assert config.generation.scheduler == "ddim_trailing"
+
+
def test_generation_reads_clip_skip_and_pad_last_block(tmp_path: Path) -> None:
path = tmp_path / "config.toml"
path.write_text(
diff --git a/tests/test_pipeline_factory.py b/tests/test_pipeline_factory.py
index 14b771b..e7904ab 100644
--- a/tests/test_pipeline_factory.py
+++ b/tests/test_pipeline_factory.py
@@ -142,6 +142,7 @@ def test_checkpoint_without_vae_fails_pipeline_initialization(app_config, monkey
),
("euler_a", "FakeEulerAncestralDiscreteScheduler", {}),
("ddim", "FakeDDIMScheduler", {}),
+ ("ddim_trailing", "FakeDDIMScheduler", {"timestep_spacing": "trailing"}),
("lcm", "FakeLCMScheduler", {}),
],
)