summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-09-12 23:36:33 +0900
committertanaka takashi <isazaka@gmail.com>2026-09-12 23:36:33 +0900
commitde44625e3baae4d43ad096248b7096578b11e0b0 (patch)
treec0c0b1756fb71f6c3f8958c3b13f86756e61390d /src
parent49d672ef4f605fddddc4040b650aed76cbf27b2a (diff)
sd_embed導入による長文プロンプト・重み付きプロンプト対応v0.1.0
SD1.5の77 token制限を回避するため、sd_embedを用いたprompt_embedsベースの 入力方式に変更した。あわせてclip_skipとpad_last_blockの設定を追加した。 sd_embedのget_weighted_text_embeddings_sd15()でpositive/negativeの プロンプト埋め込みを同時生成し、pipeline呼び出し時にprompt_embeds/ negative_prompt_embedsを渡す。prompt/negative_promptは同時に渡さない。 LoRAロード後にembedding生成を行うため、text encoder LoRAが反映された 状態で埋め込みが作られる。 sd_embedはcommit hash 4a47f71150で固定し、VCS dependencyとして追加。 hatchlingのallow-direct-referencesを有効化。 変更内容: - pyproject.toml: sd-embed @ git+...@4a47f71150追加、 allow-direct-references=true - config.py: GenerationConfigにclip_skip=0, pad_last_block=False追加、 load_configで[generation]のclip_skip/pad_last_block読込・validation (clip_skip: 0-12のinteger, pad_last_block: boolean) - model_manager.py: 3つのpipeline呼び出しをprompt/negative_promptから prompt_embeds/negative_prompt_embedsに変更、_embed_prompts()メソッド追加 - tests/test_config.py: clip_skip/pad_last_blockの正常系・異常系テスト追加 - doc/config.md: [generation]セクションにclip_skip/pad_last_block説明追記、 設定項目一覧表更新 - config.example.toml: clip_skip/pad_last_blockコメント例追記 - doc/Design.md: GenerationConfigにclip_skip/pad_last_block追記 - README.md: sd_embedの説明追記 - uv.lock: sd_embedおよび依存パッケージ追加 検証: - ruff format/check、pyright 0 errors、pytest 67 passed - 実GPU・実モデルで確認: - 短いプロンプト: 生成成功 - 77 token超の長文プロンプト(122 token): 生成成功 - 重み付きプロンプト(word:1.5)((word)): 生成成功 - LoRA(pixelartV3) + sd_embed + 重み付き: 生成成功 - negative promptの重み付き埋め込み: 生成成功
Diffstat (limited to 'src')
-rw-r--r--src/mcp_sd15/config.py14
-rw-r--r--src/mcp_sd15/model_manager.py34
2 files changed, 41 insertions, 7 deletions
diff --git a/src/mcp_sd15/config.py b/src/mcp_sd15/config.py
index e41bdfd..39f31c9 100644
--- a/src/mcp_sd15/config.py
+++ b/src/mcp_sd15/config.py
@@ -44,6 +44,8 @@ class GenerationConfig:
cfg_scale: float = 7.0
scheduler: str = "dpmpp_2m_karras"
default_size: str = "square_512"
+ clip_skip: int = 0
+ pad_last_block: bool = False
@dataclass(frozen=True)
@@ -218,6 +220,14 @@ def load_config(path: str | Path, *, validate_paths: bool = True) -> AppConfig:
default_size = _string(generation_raw, "default_size") or "square_512"
if default_size not in VALID_SIZES:
raise ConfigError(f"unsupported default_size: {default_size}")
+ clip_skip_value = generation_raw.get("clip_skip", 0)
+ if (
+ isinstance(clip_skip_value, bool)
+ or not isinstance(clip_skip_value, int)
+ or not 0 <= clip_skip_value <= 12
+ ):
+ raise ConfigError("generation.clip_skip must be an integer between 0 and 12")
+ pad_last_block = _bool(generation_raw, "pad_last_block", False)
control_raw = _table(raw, "controlnet")
controlnet = ControlNetConfig(
@@ -258,7 +268,9 @@ def load_config(path: str | Path, *, validate_paths: bool = True) -> AppConfig:
config = AppConfig(
model=ModelConfig(root, checkpoint, vae, tuple(loras)),
- generation=GenerationConfig(steps_value, cfg_scale, scheduler, default_size),
+ generation=GenerationConfig(
+ steps_value, cfg_scale, scheduler, default_size, clip_skip_value, pad_last_block
+ ),
controlnet=controlnet,
output=OutputConfig(output_dir.resolve(), _bool(output_raw, "enabled", False)),
server=ServerConfig(transport, host, port_value),
diff --git a/src/mcp_sd15/model_manager.py b/src/mcp_sd15/model_manager.py
index c2bd166..e87a813 100644
--- a/src/mcp_sd15/model_manager.py
+++ b/src/mcp_sd15/model_manager.py
@@ -31,9 +31,12 @@ class ModelManager:
def text_to_image(self, request: TextToImageRequest, seed: int) -> Image.Image:
width, height = request.size.dimensions
+ prompt_embeds, negative_prompt_embeds = self._embed_prompts(
+ request.positive_prompt, request.negative_prompt
+ )
result = self.base(
- prompt=request.positive_prompt,
- negative_prompt=request.negative_prompt,
+ prompt_embeds=prompt_embeds,
+ negative_prompt_embeds=negative_prompt_embeds,
width=width,
height=height,
num_inference_steps=self.config.generation.steps,
@@ -53,9 +56,12 @@ class ModelManager:
else:
control_image = self._openpose(image, full=request.control_mode == "openpose_full")
pipeline = self._pipeline_for(str(request.control_mode), inpaint=False)
+ prompt_embeds, negative_prompt_embeds = self._embed_prompts(
+ request.positive_prompt, request.negative_prompt
+ )
result = pipeline(
- prompt=request.positive_prompt,
- negative_prompt=request.negative_prompt,
+ prompt_embeds=prompt_embeds,
+ negative_prompt_embeds=negative_prompt_embeds,
image=control_image,
width=width,
height=height,
@@ -78,9 +84,12 @@ class ModelManager:
mask = mask.resize((width, height), Image.Resampling.NEAREST)
control_image = self._inpaint_condition(image, mask)
pipeline = self._pipeline_for("inpaint", inpaint=True)
+ prompt_embeds, negative_prompt_embeds = self._embed_prompts(
+ request.positive_prompt, request.negative_prompt
+ )
result = pipeline(
- prompt=request.positive_prompt,
- negative_prompt=request.negative_prompt,
+ prompt_embeds=prompt_embeds,
+ negative_prompt_embeds=negative_prompt_embeds,
image=image,
mask_image=mask,
control_image=control_image,
@@ -140,3 +149,16 @@ class ModelManager:
if not images or len(images) != 1 or not isinstance(images[0], Image.Image):
raise GenerationError("Diffusers did not return exactly one image")
return images[0]
+
+ def _embed_prompts(self, positive: str, negative: str) -> tuple[Any, Any]:
+ """Generate prompt embeds via sd_embed to bypass the 77-token limit."""
+ from sd_embed.embedding_funcs import get_weighted_text_embeddings_sd15
+
+ prompt_embeds, neg_prompt_embeds = get_weighted_text_embeddings_sd15(
+ self.base,
+ prompt=positive,
+ neg_prompt=negative or "",
+ pad_last_block=self.config.generation.pad_last_block,
+ clip_skip=self.config.generation.clip_skip,
+ )
+ return prompt_embeds, neg_prompt_embeds