Files
EPEEAIKit/art-agent/backend/app/services/image_prompt_strategy.py

140 lines
5.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""按生图模型预处理 prompt与 provider 解耦)。
各策略在对应函数中注明依据厂商文档、Replicate 模型 API 字段说明或社区通用写法。
统一入口prepare_image_prompt_for_model。
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
# ─── SDXLStability / 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 FLUXSubject + 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 SDXLprompt + 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