"""按生图模型预处理 prompt(与 provider 解耦)。 各策略在对应函数中注明依据:厂商文档、Replicate 模型 API 字段说明或社区通用写法。 统一入口:prepare_image_prompt_for_model。 """ from __future__ import annotations from dataclasses import dataclass from typing import Any # ─── SDXL(Stability / Replicate)──────────────────────────── # Replicate stability-ai/sdxl 提供独立字段 negative_prompt(见模型 API 页)。 # 下列为 CLIP 系文生图常见「质量/解剖」排除词,作未显式指定时的基线。 DEFAULT_SDXL_NEGATIVE = ( "low quality, worst quality, normal quality, lowres, blurry, jpeg artifacts, " "watermark, signature, text, logo, deformed, disfigured, bad anatomy, bad hands, " "extra fingers, mutated, cropped, poorly drawn face" ) @dataclass(frozen=True) class PreparedImagePrompt: """下游只读:prompt 必填;negative_prompt 仅部分后端使用(当前为 SDXL)。""" prompt: str negative_prompt: str | None = None def prepare_image_prompt_for_model( model_config: dict[str, Any], raw_prompt: str, *, has_reference_images: bool = False, ) -> PreparedImagePrompt: """根据注册表 id 选择策略。未知 id 时原样透传。""" short_id = model_config.get("id", "") text = (raw_prompt or "").strip() if not text: return PreparedImagePrompt(prompt="") dispatch: dict[str, Any] = { "gpt-image-1.5": _openai_gpt_image, "gemini-3.1-flash-image": _gemini_native_image, "flux-schnell": _flux_bfl, "flux-dev": _flux_bfl, "sdxl": _sdxl_replicate, "instant-style": _replicate_ip_adapter_scene, "kolors-ipadapter": _replicate_ip_adapter_scene, } fn = dispatch.get(short_id, _passthrough) return fn(text, model_config, has_reference_images) def _passthrough(text: str, _model_config: dict[str, Any], _has_ref: bool) -> PreparedImagePrompt: return PreparedImagePrompt(prompt=text) def _openai_gpt_image(text: str, _model_config: dict[str, Any], _has_ref: bool) -> PreparedImagePrompt: """OpenAI GPT Image:自然语言指令遵循强,宜为完整、具体的场景描述。 参考:https://platform.openai.com/docs/guides/image-generation """ return PreparedImagePrompt(prompt=_collapse_ws(text)) def _gemini_native_image(text: str, _model_config: dict[str, Any], _has_ref: bool) -> PreparedImagePrompt: """Gemini 原生生图:多模态指令 + 文本;英文描述通常效果稳定。 参考:Google AI 文档 generateContent 与图像输出 modality 说明。 """ return PreparedImagePrompt(prompt=_collapse_ws(text)) def _flux_bfl(text: str, _model_config: dict[str, Any], _has_ref: bool) -> PreparedImagePrompt: """Black Forest Labs FLUX:Subject + Action + Style + Context;靠前放置重点;无 negative API。 参考:https://docs.bfl.ai/guides/prompting_guide_t2i_fundamentals 若用户从 SDXL 复制了 Negative 段,尽量剥掉以免干扰文意。 """ cleaned = _strip_pasted_negative_block(text) return PreparedImagePrompt(prompt=_collapse_ws(cleaned)) def _sdxl_replicate(text: str, model_config: dict[str, Any], _has_ref: bool) -> PreparedImagePrompt: """Replicate SDXL:prompt + negative_prompt 双字段;支持显式拆分。 约定(可选):正提示与负提示用单独一行分隔符,便于 Agent/用户手写。 - ---NEGATIVE--- 或 |||NEG||| 未拆分时使用 default_params.negative_prompt 或模块默认 DEFAULT_SDXL_NEGATIVE。 """ neg_fallback = model_config.get("default_params", {}).get( "negative_prompt", DEFAULT_SDXL_NEGATIVE ) if "---NEGATIVE---" in text: pos, _, neg = text.partition("---NEGATIVE---") pos = pos.strip() neg = neg.strip() return PreparedImagePrompt( prompt=_collapse_ws(pos), negative_prompt=neg or neg_fallback, ) if "|||NEG|||" in text: pos, _, neg = text.partition("|||NEG|||") pos = pos.strip() neg = neg.strip() return PreparedImagePrompt( prompt=_collapse_ws(pos), negative_prompt=neg or neg_fallback, ) return PreparedImagePrompt( prompt=_collapse_ws(text), negative_prompt=neg_fallback, ) def _replicate_ip_adapter_scene( text: str, _model_config: dict[str, Any], has_reference_images: bool ) -> PreparedImagePrompt: """IP-Adapter / InstantStyle / Kolors:参考图承担风格与纹理,prompt 侧重场景与内容语义。 Replicate 各模型 README 均强调 prompt + 参考图配合;无参考图时由上层拦截。 有参考图时不额外堆叠长前缀,避免稀释主体描述。 """ _ = has_reference_images return PreparedImagePrompt(prompt=_collapse_ws(text)) def _collapse_ws(s: str) -> str: return " ".join(s.split()) def _strip_pasted_negative_block(text: str) -> str: lower = text.lower() for sep in ("\n---negative---\n", "\nnegative prompt:", "\nnegative:"): idx = lower.find(sep) if idx != -1: return text[:idx].strip() return text